728x90

IntelliJ IDEA 에서 GitHub 로 자료를 업로드하다보니 잘못하여 작성한 자료가 전부 날라가는 현상을 겪었다.

 

위와 같은 명령어를 사용하여 복구를 하고 나서 아예 다른 폴더로 파일을 옮겨서 업로드하기로 했다.

 

GitHub 에 첫번째 업로드를 stage01 로 업로드하고 다음 내용이 추가된 것은 stage02 로 업로드하기로 하고 시작했는데 업로드가 제대로 안되는 거다.

 

git branch stage02 로 새 브랜치를 생성만 하고, checkout 으로 해당 브랜치에 이동하지 않아서 생긴 문제였다.

# stage01에 있는 최신 커밋을 stage02로도 가져오기
git checkout stage02
git merge stage01

# 다시 PUSH
git push -f origin stage02

# branch 를 만들면서 이동하려면 반드시 아래 명령어를 사용한다.
git checkout -b stage02

728x90

'자료 구하기 > Git' 카테고리의 다른 글

소스트리 설치  (0) 2021.02.18
Git 기본 명령어 및 GitHub에 업로드  (0) 2020.08.24
윈도우10 Git 설치  (0) 2020.08.23
블로그 이미지

Link2Me

,
728x90

Nginx 설정으로 8080 포트 접근을 차단 설정하면 문제가 발생하더라.

Spring Boot 포트를 8080으로 설정해서 jar 파일을 구동시켰다.

1
2
nohup java -Xshare:off -jar /home/spring/SpringJWT/SpringJWT-0.0.1-SNAPSHOT.jar --server.port=8080 > /var/log/springjwt.log 2>&1 &
 



방화벽 설정을 아래와 같이 하면 https://spb.abc.com:8080/api/pub-key 와 같은 직접적인 접속을 허용하지 않는다.

오로지 Front-End 언어(ex, React)에서 Back-End 를 호출하는 axios 라이브러리를 통한 접속만 허용한다.

1
2
3
4
5
6
7
8
9
10
11
12
ㅇ 현재 방화벽 설정 확인
sudo firewall-cmd --list-all
 
ㅇ 8080 포트를 오직 로컬(Nginx)에서만 접근 가능하도록 설정
# Nginx 프록시 서버만 8080 포트 접근 허용 (예: 127.0.0.1 또는 내부 IP)
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="127.0.0.1" port port="8080" protocol="tcp" accept'
 
# 외부에서 8080 포트 접근 차단
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" port port="8080" protocol="tcp" drop'
 
# 방화벽 재적용
sudo firewall-cmd --reload

 

 

nginx.conf 설정 예제 파일

실제 클라우드 환경에서 동작 테스트한 스크립트이며 도메인만 다른 명칭으로 변경했다.

즉 abc 라고 된 부분만 실제 부분을 변경한 것이다.

rocky linux 9.5 가 openssl 버전을 TLS 1.3 버전을 지원해서 아래와 같이 수정했다.

만약 TLS 1.2 버전을 지원하면 ssl_ciphers 주석 부분을 살리고 이에 맞는 스크립트로 수정해야 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
worker_processes auto;
 
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
 
include /etc/nginx/modules-enabled/*.conf;
 
events {
    worker_connections 1024;
}
 
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
 
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
 
    access_log /var/log/nginx/access.log main;
 
    sendfile on;
    keepalive_timeout 65;
 
    # 시스템 정보 노출 제한
    server_tokens off;
 
    # 디렉토리 검색 방지
    autoindex off;
 
    ssl_protocols TLSv1.3;
    ssl_ecdh_curve X25519:prime256v1:secp384r1;
    #ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;
 
    # Redirect all HTTP traffic to HTTPS for spb.abc.com
    server {
        listen 80;
        server_name spb.abc.com;
        return 301 https://spb.abc.com$request_uri;
    }
 
    # Reverse Proxy for spb.abc.com with React static files
    server {
        listen 443 ssl;
        server_name spb.abc.com;
 
        ssl_certificate /etc/letsencrypt/live/spb.abc.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/spb.abc.com/privkey.pem;
        ssl_trusted_certificate /etc/letsencrypt/live/spb.abc.com/chain.pem;
        include /etc/letsencrypt/options-ssl-nginx.conf;
 
        error_log /var/log/nginx/spb-error.log;
        access_log /var/log/nginx/spb-access.log main;
 
        root /home/react/react-auth/build;
        index index.html;
 
        location / {
            try_files $uri /index.html;
            limit_except GET POST {
                deny all;
            }
        }
 
        location /static/ {
            root /home/react/react-auth/build/static;
            expires 30d;
            add_header Cache-Control "public, max-age=2592000";
        }
 
        location ~* \.(?:js|css|json|ico|png|jpg|jpeg|svg|woff|woff2|ttf|otf|eot|mp4|webm|ogv|ogg|mp3|wav|webp)$ {
            expires max;
            access_log off;
            add_header Cache-Control "public";
        }
 
    location ^~ /api {
        proxy_pass https://spb.abc.com:8080/api;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
 
    location ^~ /login {
        proxy_pass https://spb.abc.com:8080/login;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    }
 
    include /etc/nginx/conf.d/*.conf;
}
 

 

 

아래 내용은 chatGPT 도움을 받은 내용을 기록해 둔다.

 

location ^~ /api {
    proxy_pass https://spb.abc.com:8080/api;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

이 설정은 Nginx가 /api 경로로 들어오는 요청을 https://spb.abc.com:8080/api로 프록시(Proxy)하는 역할을 한다.
즉, Front-End 가 https://spb.abc.com/api로 요청을 보내면, 해당 요청이 https://spb.abc.com:8080/api로 전달된다.

location ^~ /api {
    /api로 시작하는 모든 요청을 처리한다.
    ^~ : 해당 경로가 요청 URL과 일치하면 더 이상 다른 location 블록을 검사하지 않음 (우선순위 높음).

proxy_pass https://spb.abc.com:8080/api;
    Nginx가 /api 요청을 https://spb.abc.com:8080/api로 전달한다.
    예를 들어:

    React 요청:  https://spb.abc.com/api/users
    Nginx가 전달하는 요청: https://spb.abc.com:8080/api/users

    proxy_pass는 백엔드 서버(8080번 포트에서 실행 중인 API 서버)로 요청을 전달하는 역할을 한다.

# proxy_set_header 설정
proxy_set_header Host $host;
    원본 요청의 Host 헤더를 유지한다.
    기본적으로 proxy_pass를 사용하면 Nginx는 원래 요청의 Host 헤더를 변경할 수 있다.
    Host 헤더를 유지하면 백엔드 서버에서 도메인 정보를 유지한 채 요청을 처리할 수 있음.


proxy_set_header X-Real-IP $remote_addr;
    클라이언트(사용자)의 실제 IP 주소를 백엔드 서버로 전달한다.
    예를 들어:

    클라이언트 IP: 123.45.67.89
    백엔드 서버에서 보는 IP: 123.45.67.89

    Nginx가 백엔드 서버를 대신 호출하기 때문에, 백엔드에서는 보통 클라이언트의 IP를 127.0.0.1로 인식할 수 있다.
    이 설정이 없으면 백엔드 서버는 모든 요청이 Nginx에서 온 것으로 인식할 수 있음.


proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    X-Forwarded-For 헤더는 클라이언트의 IP 주소를 보존하는 역할을 한다.
    예를 들어, 사용자가 여러 프록시를 거쳐 요청하는 경우:

    X-Forwarded-For: 123.45.67.89, 98.76.54.32, 192.168.1.10

        123.45.67.89 → 실제 클라이언트 IP
        98.76.54.32 → 중간 프록시
        192.168.1.10 → 마지막 프록시(Nginx)
    백엔드에서 로그를 분석할 때 실제 클라이언트의 IP를 확인할 수 있음.


proxy_set_header X-Forwarded-Proto $scheme;
    클라이언트가 요청할 때 사용한 프로토콜(HTTP 또는 HTTPS)을 백엔드 서버에 전달한다.
    예를 들어:

    클라이언트가 https://spb.abc.com/api 요청 → X-Forwarded-Proto: https
    클라이언트가 http://spb.abc.com/api 요청 → X-Forwarded-Proto: http

    백엔드에서 HTTPS 요청을 식별할 때 유용하다.

 

 

1
2
3
4
5
6
7
location /gis {
    proxy_pass http://192.168.123.91:18080;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}
 

 

location /gis {
    /gis로 시작하는 모든 요청을 처리한다.
    예를 들어:

    클라이언트 요청:  http://spb.abc.com/gis/map
    Nginx가 전달하는 요청: http://192.168.123.91:18080/map
    /gis는 프록시 서버에서 제거되므로, 백엔드 서버(192.168.123.91:18080)는 /map으로 요청을 받음.


proxy_pass http://192.168.123.91:18080;
    백엔드 서버(192.168.123.91:18080)로 요청을 전달한다.
    예제:
    클라이언트 요청: http://spb.abc.com/gis/data
    Nginx가 전달하는 요청: http://192.168.123.91:18080/data
    /gis 경로는 자동으로 제거되며, 백엔드 서버가 그대로 요청을 처리할 수 있도록 설정됨.


만약 /gis를 유지해야 한다면, 다음처럼 수정 필요
proxy_pass http://192.168.123.91:18080/gis;

이렇게 하면 백엔드 서버에서 /gis/data 형태로 요청을 받게 된다.


proxy_http_version 1.1;
    백엔드와의 통신에서 HTTP/1.1을 사용하도록 설정.
    기본적으로 Nginx는 HTTP/1.0을 사용할 수 있음.
    HTTP/1.1을 사용하면 WebSocket과 같은 기능을 지원할 수 있음.


proxy_set_header Host $host;
    원본 요청의 Host 헤더를 유지하여 백엔드 서버로 전달한다.
    예를 들어:
    클라이언트 요청 Host: spb.abc.com
    백엔드 서버에서 보는 Host: spb.abc.com

    백엔드가 요청을 받을 때 Nginx의 IP 대신 원본 도메인 정보를 유지할 수 있음.


proxy_set_header X-Real-IP $remote_addr;
    클라이언트의 실제 IP를 백엔드 서버에 전달한다.
    기본적으로 Nginx가 백엔드에 요청을 전달할 때 자신(Nginx)의 IP로 요청을 보내기 때문에, 
    백엔드 서버에서는 클라이언트의 원래 IP를 확인할 수 없음.
    이 설정을 추가하면 백엔드가 원래 클라이언트의 IP를 알 수 있음.

728x90
블로그 이미지

Link2Me

,
728x90

implementation 'org.springframework.boot:spring-boot-starter-web' 라이브러리는 Back-end 단에서 필요한 의존성입니다. React는 Front-end 라이브러리로 JavaScript 기반에서 동작하며, 서버와의 통신은 보통 HTTP 요청(REST API) 또는 WebSocket을 사용합니다. 이 의존성은 Spring Boot에서 HTTP 요청을 처리하거나 RESTful API를 제공하기 위해 필요한 설정과 구성 요소(Web 관련 모듈 포함)를 포함하고 있습니다.

의존성의 역할

  • spring-boot-starter-web 의존성을 추가하면 다음과 같은 기능을 사용할 수 있습니다:
    1. Spring MVC: 컨트롤러(endpoint)를 정의하기 위한 기능.
    2. 내장 톰캣 서버: 애플리케이션을 실행하기 위한 웹 서버.
    3. Jackson 라이브러리 포함: JSON 형식의 요청/응답 바인딩.
    4. 기타 Spring Web 관련 설정 및 모듈.

따라서 React가 Front-end로 쓰이고, Back-end를 Spring Boot로 구현한다면 API 서버를 구축하기 위해 반드시 필요합니다. React와 Spring Boot는 별개이지만, React에서 만들어진 요청을 Spring Boot가 처리하려면 spring-boot-starter-web 의존성은 필수입니다.

728x90
블로그 이미지

Link2Me

,
728x90

Webstorm 에서 코드의 의미가 어떻게 다른지 AI에게 문의를 해봤다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const handleChangeTodo = (e) => {
 
todo[e.target.name= e.target.value
// e.target.name : 입력 필드의 `name` 속성 값
// e.target.value : 사용자가 입력한 실제 데이터
 
setTodo({...todo})
}
 
위 코드와 아래 코드는 동일한 결과를 얻는가?
 
const handleChangeTodo = (e) => {
setTodo((prevTodo) => ({
...prevTodo,
[e.target.name]: e.target.value,
}));
};
 

 

첫번째 코드

- 여기서 `todo[e.target.name] = e.target.value`를 통해 기존 상태 객체인 `todo`를 **직접 수정**하고, 
  이후 스프레드 연산자를 사용해 새로운 객체를 생성하여 `setTodo`를 호출하고 있다.

- **문제점**:
- React는 상태를 직접 수정하는 것을 권장하지 않는다. 
  직접 수정(`todo[e.target.name] = e.target.value`) 시 예상치 못한 상태 동작이나 렌더링 문제가 발생할 가능성이 있다.
- 객체 복사를 통해 새로운 상태를 생성한다고는 해도, `todo` 객체는 이미 수정된 상태이므로 **불변성을 위반**한 상태가 된다.

 

두번째 코드

- 이 코드는 `setTodo`에서 이전 상태(`prevTodo`)를 기반으로 새로운 상태를 생성한다.
  기존 상태(`prevTodo`)는 불변성을 유지한 채 동적으로 수정된 속성 값만 변경된다.
- **장점**:
- `prevTodo`는 최신 상태를 안전하게 참조하며, 새로운 객체를 생성하여 React의 상태 업데이트 방식에 맞춰 동작한다.
- 이 방식은 React의 상태 불변성 원칙에 부합하며, React의 상태 변경 및 렌더링 로직에서 **안전하게 작동**한다.

 

webstorm 에 연결해서 유료로 사용하는 AI 는 코드 내용 전체를 알기 때문에 chatGPT에 문의하는 것보다 설명의 정확도 좀 더 나은 거 같기도 하다.

하지만 chatGPT 를 통해서는 다양한 걸 문의하는 장점이 있는 거 같다.

728x90

'React > React' 카테고리의 다른 글

React 배포를 위한 nginx 설정  (0) 2025.03.13
React Naver Map API 사용 샘플  (2) 2022.10.25
React 카카오맵 API 사용 샘플  (0) 2022.10.24
React useMemo & useCallback  (0) 2022.10.24
React useRef 사용 예제  (0) 2022.10.15
블로그 이미지

Link2Me

,
728x90

서울 중구청 조직도를 파싱처리하는 Python 코드이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import requests
from bs4 import BeautifulSoup
import pandas as pd
import re
 
def crawl_orgchart_guchung(url):
    # 요청 및 응답 확인
    headers = {
        "User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
    }
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.text, "html.parser")
 
    # 데이터 저장 리스트
    data = []
 
    # 조직도의 범주에 해당하는 코드를 찾아낸다.
    scope = soup.find("div", class_="jg_organization_chart")
    # print(scope)
 
    # 모든 href 속성값 추출
    href_values = [a.get('href'for a in scope.find_all('a', href=True)]
 
    # '#' 또는 'javascript:' 를 포함하지 않는 href만 필터링하고,
    # 'https://www.junggu.seoul.kr'이 포함되지 않으면 추가하여 완전한 URL로 반환
    base_url = "https://www.junggu.seoul.kr"
    filtered_href = [
        href if href.startswith("https://www.junggu.seoul.kr"else base_url + href
        for href in href_values
        if not href.startswith(("#""javascript:"))
    ]
 
    # "dong" 포함 여부에 따라 분리
    dong_href = [href for href in filtered_href if "dong" in href]
    non_dong_href = [href for href in filtered_href if "dong" not in href]
 
    data = [non_dong_href, dong_href]
    return data
 
def crawl_orgchart_main(url):
    # 요청 및 응답 확인
    headers = {
        "User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
    }
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.text, "html.parser")
 
    # 데이터 저장 리스트
    data = []
 
    # 테이블 찾기
    tables = soup.find_all("div",class_="tableScroll")
 
    for table in tables:
        # print(table)
        rows = table.select("tbody tr")
        # print(rows)
        for row in rows:
            team = row.find("th").text.strip()
            cols = row.find_all("td")
            if len(cols) >= 3:  # 필요한 열 개수 확인
                position = cols[0].text.strip()
                phone = cols[1].text.strip()
                duty = cols[2].text.strip()
                data.append([team, position, phone, duty])
    
    # 데이터프레임 생성
    columns = ["부서명""직위""전화번호","담당업무"]
    df = pd.DataFrame(data, columns=columns)
    return df
 
def crawl_orgchart_dong(url):
 
    # 요청 및 응답 확인
    headers = {
        "User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
    }
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.text, "html.parser")
 
    # 데이터 저장 리스트
    data = []
 
    # 조직도의 범주에 해당하는 코드를 찾아낸다.
    scope = soup.find("div", class_="member_list")
 
    # 테이블 찾기
    rows = scope.select("table tbody tr")
    for row in rows:
        team = row.find("th").text.strip()
        cols = row.find_all("td")
        if len(cols) >= 3:  # 필요한 열 개수 확인
            position = " ".join(cols[0].stripped_strings)
            phone = cols[1].get_text(separator=" ").strip()
            duty = cols[2].get_text(separator=" ").strip()
            data.append([team, position, phone, duty])
 
    # 데이터프레임 생성
    columns = ["부서명""직위""전화번호""담당업무"]
    df = pd.DataFrame(data, columns=columns)
    return df
 
if __name__ == "__main__":
    url_main = "https://www.junggu.seoul.kr/content.do?cmsid=14066"
    df_main = crawl_orgchart_guchung(url_main)
 
    # 모든 URL에서 데이터 크롤링
    dataframe_main = [crawl_orgchart_main(url) for url in df_main[0]]
 
    # 데이터프레임 병합
    df_combined = pd.concat(dataframe_main, ignore_index=True)
    # 두 데이터프레임을 합치기
    if not df_combined.empty:
        print(df_combined)
        df_combined.to_csv("서울중구청.csv", index=False, encoding="utf-8-sig")
 
    dataframe_dong = [crawl_orgchart_dong(url) for url in df_main[1]]
    # 데이터프레임 병합
    df_dong_combined = pd.concat(dataframe_dong, ignore_index=True)
    # 두 데이터프레임을 합치기
    if not df_dong_combined.empty:
        print(df_dong_combined)
        df_dong_combined.to_csv("서울중구청_동주민센터.csv", index=False, encoding="utf-8-sig")
 

 

총 3단계의 과정으로 진행하는 코드이다.

 

자료를 추출하는 과정에 대한 설명은 생략한다.

728x90
블로그 이미지

Link2Me

,
728x90

WebStorm(웹스톰)의 경우 JavaScript 개발을 목적으로한 IDE이기 때문에 기본 환경으로도 개발하는데 충분하다. 

WebStorm 에서 자동 완성하는 단축키를 적어둔다.

rsf 를 입력하고 엔터키를 치면 함수형 컴포넌트가 자동완성된다.

 

 

 

 

단축키 : rsc Tab키 또는 rsc 엔터키

 

728x90
블로그 이미지

Link2Me

,
728x90

Rocky 9.5 리눅스 환경에서 스프링부트 실행을 위해 준비한 스크립트이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
##############################################
######### RockeyOS 9.5 Spring Boot 설치 ######
##############################################
# 스크립트는 반드시 관리자 권한에서 실행해야 한다.
# openssl 버전 확인 => TLS 1.3 지원
openssl version
 
dnf -y install expat-devel
 
# 시스템 업데이트
sudo dnf -y update
 
sudo dnf -y install wget unzip mc git nmap curl
 
# SSL 관련 패키지 설치 확인
sudo dnf -y install openssl openssl-devel
 
####################################################################################
# 방화벽 데몬 시작
systemctl start firewalld
 
# 서버 부팅 시 firewalld 데몬 자동 시작 설정
systemctl enable firewalld
 
firewall-cmd --permanent --add-service=http 
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --add-service=mysql
firewall-cmd --permanent --zone=public --add-port=3306/tcp
 
firewall-cmd --permanent --zone=public --add-port=8080/tcp
firewall-cmd --permanent --zone=public --add-port=3000/tcp
firewall-cmd --reload
firewall-cmd --list-all
 
####################################################################################
# tomcat 배포 사전 조건
# 1. JAVA가 설치되어 있어야 한다. (톰캣 9는 자바 11 이상이 필요)
# jar 파일은 내부 톰캣이 있기에 따로 톰캣을 설치하지 않아도 된다. 
####################################################################################
# 1. 기본 패키지 업데이트
sudo dnf update -y
 
# 2. Java 17 버전 설치
sudo dnf install -y java-17-openjdk java-17-openjdk-devel
java -version
 
# 3. Java 환경 변수 설정
echo "export JAVA_HOME=/usr/lib/jvm/java-17-openjdk" | sudo tee -/etc/profile.d/java.sh
echo "export PATH=\$JAVA_HOME/bin:\$PATH" | sudo tee -/etc/profile.d/java.sh
source /etc/profile.d/java.sh
 
# 여러개 설치되어 있는 자바 버전 선택하기
sudo alternatives --config java
 
 
# 4. Spring Boot 실행
# 아래 명령어를 수행하여 실행되는 메시지를 직접 육안으로 확인한다.
java -jar <jar파일명>
java -Xshare:off -jar /home/spring/SpringJWT/SpringJWT-0.0.1-SNAPSHOT.jar
 
# 5. Spring Boot 애플리케이션 실행
# 애플리케이션 실행 (백그라운드)
# 노트북에서 빌드(build)한 파일을 리눅스 서버로 업로드하고 아래와 같이 실행한다.
 
nohup java -Xshare:off -jar /home/spring/SpringJWT/SpringJWT-0.0.1-SNAPSHOT.jar --server.port=8080 > /var/log/springjwt.log 2>&1 &
 
 
# 필요한 Apache 모듈 활성화
sudo systemctl restart httpd
 
####################################################################################
# 프로세스 확인
ps aux | grep SpringJWT
 
# 프로세스 종료 (필요 시)
kill $(ps aux | grep 'SpringJWT' | awk '{print $2}')
 
# 종료 프로세스 작성
vi /usr/local/bin/kill_8080.sh
 
#!/bin/bash
 
# 8080 포트 사용 중인 프로세스 확인
PID=$(lsof -i :8080 -t)
 
if [ -"$PID" ]; then
    echo "8080 포트에서 실행 중인 프로세스(PID: $PID) 종료 중..."
    kill $PID
    sleep 2
 
    # 프로세스가 종료되지 않았으면 강제 종료
    if ps -p $PID > /dev/null; then
        echo "⚠️ 정상 종료되지 않아 강제 종료 진행..."
        kill -9 $PID
    fi
 
    echo "8080 포트 프로세스 종료 완료."
else
    echo "ℹ️ 8080 포트에서 실행 중인 프로세스가 없습니다."
fi
 
# 저장(wq)하고 빠져나온다.
 
# 실행권한 부여
sudo chmod +/usr/local/bin/kill_8080.sh
 
# 스크립트 실행
sudo /usr/local/bin/kill_8080.sh
 
####################################################################################
# systemd 서비스 등록 (서버 부팅 후 자동 실행)
vi /etc/systemd/system/springjwt.service
 
[Unit]
Description=Spring Boot Application - SpringJWT
After=network.target
 
[Service]
User=apache
Group=apache
ExecStart=/usr/bin/java -Xshare:off -jar /home/spring/SpringJWT/SpringJWT-0.0.1-SNAPSHOT.jar
SuccessExitStatus=143
Restart=always
RestartSec=5
StandardOutput=file:/var/log/springjwt.log
StandardError=file:/var/log/springjwt.log
 
[Install]
WantedBy=multi-user.target
 
#저장(wq)하고 빠져나온다.
 
# systemd 데몬 리로드
sudo systemctl daemon-reload
 
# 서비스 등록 및 실행
sudo systemctl enable springjwt
sudo systemctl start springjwt
 
# 실행 상태 확인
sudo systemctl status springjwt
 
# 서비스 중지 (필요 시)
sudo systemctl stop springjwt
 
# 실행 로그 확인
cat /var/log/springjwt.log
 
####################################################################################
 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

React 샘플 프로젝트 배포를 Apache 로 설정하려다가 포기했다. 이미 환경설정이 PHP로 VirtualHost 설정이 되어 있어서 문제가 되는가 싶어 Apache stop 을 시키고 nginx 설치를 했다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
worker_processes auto;
 
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
 
include /etc/nginx/modules-enabled/*.conf;
 
events {
    worker_connections 1024;
}
 
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
 
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
 
    access_log /var/log/nginx/access.log main;
 
    sendfile on;
    keepalive_timeout 65;
 
    # 시스템 정보 노출 제한
    server_tokens off;
 
    # 디렉토리 검색 방지
    autoindex off;
 
    ssl_protocols TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;
 
    # Redirect all HTTP traffic to HTTPS for spb.abc.com
    server {
        listen 80;
        server_name spb.abc.com;
        return 301 https://spb.abc.com$request_uri;
    }
 
    # Reverse Proxy for spb.abc.com with React static files
    server {
        listen 443 ssl;
        server_name spb.abc.com;
 
        ssl_certificate /etc/letsencrypt/live/spb.abc.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/spb.abc.com/privkey.pem;
        ssl_trusted_certificate /etc/letsencrypt/live/spb.abc.com/chain.pem;
        include /etc/letsencrypt/options-ssl-nginx.conf;
 
        error_log /var/log/nginx/spb-error.log;
        access_log /var/log/nginx/spb-access.log main;
 
        root /home/react/react-auth/build;
        index index.html;
 
        location / {
            try_files $uri /index.html;
            limit_except GET POST {
                deny all;
            }
        }
 
        location /static/ {
            root /home/react/react-auth/build;
            expires 30d;
            add_header Cache-Control "public, max-age=2592000";
        }
 
        location ~* \.(?:js|css|json|ico|png|jpg|jpeg|svg|woff|woff2|ttf|otf|eot|mp4|webm|ogv|ogg|mp3|wav|webp)$ {
            expires max;
            access_log off;
            add_header Cache-Control "public";
        }
    }
 
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Referrer-Policy "no-referrer-when-downgrade";
 
    include /etc/nginx/conf.d/*.conf;
}
 

 

 

Rocky Linux 9.5 가 TLS 1.3을 지원하기 때문에 1.3버전만 명시했다.

 

아래 스크립트는 nginx 설치과정부터 설명한 내역이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
####################################################################
# Apache를 설치한 경우에는 아파치 동작 중지
systemctl stop httpd
 
####################################################################
# nginx 설치 과정
####################################################################
# 패키지 관리자 업데이트
dnf -y update
 
# NGINX RPM 패키지 도구(dnf, yum)로 설치하는 방법
dnf -y install yum-utils
 
# NGINX 공식 저장소를 추가
vi /etc/yum.repos.d/nginx.repo
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
 
[nginx-mainline]
name=nginx mainline repo
baseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/
gpgcheck=1
enabled=0
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
 
#wq(저장하고 빠져나오기)
 
 
# NGINX 최신 버전과 버그 수정을 포함하는 mainline 버전을 사용하려면 다음 명령어를 실행
yum-config-manager --enable nginx-mainline
 
#  버그 수정만 포함한 안정 버전을 사용하려면 다음 명령어을 실행
yum-config-manager --enable nginx-stable
 
# NGINX 패키지 설치 ==> NGINX 공식 저장소에서 최신 버전의 NGINX 패키지를 다운로드하고 설치
dnf -y install nginx
 
# NGINX 버전 확인
nginx -v
 
####################################################################
# nginx 데몬 실행
####################################################################
systemctl start nginx
systemctl enable nginx
systemctl status nginx
 
# 환경파일 수정 
vi /etc/nginx/nginx.conf
 
worker_processes auto;
 
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
 
include /etc/nginx/modules-enabled/*.conf;
 
events {
    worker_connections 1024;
}
 
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
 
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
 
    access_log /var/log/nginx/access.log main;
 
    sendfile on;
    keepalive_timeout 65;
 
    # 시스템 정보 노출 제한
    server_tokens off;
 
    # 디렉토리 검색 방지
    autoindex off;
 
    ssl_protocols TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;
 
    # Redirect all HTTP traffic to HTTPS for spb.abc.com
    server {
        listen 80;
        server_name spb.abc.com;
        return 301 https://spb.abc.com$request_uri;
    }
 
    # Reverse Proxy for spb.abc.com with React static files
    server {
        listen 443 ssl;
        server_name spb.abc.com;
 
        ssl_certificate /etc/letsencrypt/live/spb.abc.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/spb.abc.com/privkey.pem;
        ssl_trusted_certificate /etc/letsencrypt/live/spb.abc.com/chain.pem;
        include /etc/letsencrypt/options-ssl-nginx.conf;
 
        error_log /var/log/nginx/spb-error.log;
        access_log /var/log/nginx/spb-access.log main;
 
        root /home/react/react-auth/build;
        index index.html;
 
        location / {
            try_files $uri /index.html;
            limit_except GET POST {
                deny all;
            }
        }
 
        location /static/ {
            root /home/react/react-auth/build;
            expires 30d;
            add_header Cache-Control "public, max-age=2592000";
        }
 
        location ~* \.(?:js|css|json|ico|png|jpg|jpeg|svg|woff|woff2|ttf|otf|eot|mp4|webm|ogv|ogg|mp3|wav|webp)$ {
            expires max;
            access_log off;
            add_header Cache-Control "public";
        }
    }
 
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Referrer-Policy "no-referrer-when-downgrade";
 
    include /etc/nginx/conf.d/*.conf;
}
 
# 저장하고 나온다.
 
# 설정 테스트
sudo nginx -t
 
#설정 반영 및 Nginx 재시작
systemctl restart nginx
 
#Ownership
chown nginx:nginx -/home/react/react-auth/build
 

 

 

React 빌드 및 nginx 재시작

 

 

 

728x90

'React > React' 카테고리의 다른 글

React 코드 비교를 통한 의미 이해  (0) 2025.03.19
React Naver Map API 사용 샘플  (2) 2022.10.25
React 카카오맵 API 사용 샘플  (0) 2022.10.24
React useMemo & useCallback  (0) 2022.10.24
React useRef 사용 예제  (0) 2022.10.15
블로그 이미지

Link2Me

,
728x90

React 에서 axios 를 통해서 백엔드에서 키를 호출하는 방법에 대해 기록한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import React from "react";
import {useState, useEffect, useRef} from "react";
import {Link, useNavigate} from "react-router-dom";
import axios from "axios";
import { JSEncrypt } from "jsencrypt"// npm install jsencrypt
 
const Signup = () => {
    //const baseUrl = process.env.REACT_APP_API_BASE_URL || "http://localhost:8080";
    const baseUrl = "https://spb.abc.com:8080";
 
    const navigate = useNavigate();
    const initData = {
        userID: '',
        password: ''
    }
 
    const [publicKey, setPublicKey] = useState("");
    const [sessionId, setSessionId] = useState(""); // 세션 ID 저장
    const isFetched = useRef(false); // 실행 여부 체크
 
    useEffect(() => {
        if (isFetched.current) return
        isFetched.current = true
 
        console.log("🔹 공개키 요청 실행됨");
        // 백엔드에서 공개키 가져오기
        axios.get(baseUrl + "/api/public-key")
            .then(response => {
                console.log("🔹 공개키 응답:", response.data);
                setPublicKey(response.data.publicKey);
                setSessionId(response.data.sessionId); // 세션 ID 저장
            })
            .catch(error => console.error("공개키 가져오기 실패", error));
    }, []);
 
    const encrypt = new JSEncrypt(); // Start our encryptor.
    encrypt.setPublicKey(publicKey); // Assign our encryptor to utilize the public key.
 
    const [formData, setFormData] = useState(initData);
    const [users, setUsers] = useState([]);
    // 첫번째 원소 : 현재 상태, 두번재 원소 : 상태를 바꾸어 주는 함수
    const [error, setError] = useState(null);
 
    const onChangeInput = (e) => {
        setFormData({
            ...formData,
            [e.target.name]: e.target.value
        });
        // e.target.name 은 해당 input의 name을 가리킨다.
        //console.log(formData);
    }
 
    const submitForm = (e) => {
        e.preventDefault();
        
        if (!sessionId) {
            console.error("세션 ID가 없습니다. 공개키를 다시 요청하세요.");
            setError(true);
            return;
        }
 
        const params = {
            username: formData.userID,
            password: encrypt.encrypt(formData.password), // 비밀번호 암호화
            //password: formData.password, 
            sessionId: sessionId // 세션 ID 포함
        }
        console.log(params);
        axios.post(baseUrl + '/login', params)
            .then((res) => {
                console.log(res);
                if (res.status === 200) {
                    console.log(res.data);
                    localStorage.setItem("accessToken", res.data.token);
                    axios.defaults.headers.common["Authorization"= "Bearer " + res.data.token;
                    navigate('/');
                } else {
                    console.log(res.data.message);
                    setError(true);
                }
            })
            .catch(error => {
                console.log(error.response)
            });
    }
 
    return (
        <div className="container h-100 mt-5">
            <div className="row d-flex justify-content-center align-items-center h-100">
                <div className="col-12 col-md-9 col-lg-7 col-xl-6">
                    <div className="card">
                        <div className="card-body p-5">
                            <h2 className="text-uppercase text-center mb-5">로그인</h2>
                            <form onSubmit={submitForm}>
                                <div className="form-outline mb-4">
                                    <label className="form-label" htmlFor="userID">userID</label>
                                    <input type="text" name="userID" onChange={onChangeInput} id="userID"
                                            className="form-control form-control-lg" value={formData.userID}
                                            required/>
                                </div>
 
                                <div className="form-outline mb-4">
                                    <label className="form-label" htmlFor="password">Password</label>
                                    <input type="password" name="password" onChange={onChangeInput} id="password"
                                            className="form-control form-control-lg" value={formData.password}
                                            required/>
                                </div>
 
                                { error &&
                                <div className="alert alert-danger" role="alert">
                                    로그인 정보를 다시 한번 확인하세요!
                                </div>
                                }
 
                                <div className="d-flex justify-content-center">
                                    <button type="submit"
                                            className="btn btn-primary btn-block btn-lg gradient-custom-3 text-body">로그인
                                    </button>
                                </div>
 
                                <p className="text-center text-muted mt-5 mb-0">
                                    <Link to="/register" className="fw-bold text-body"><u>회원가입</u></Link>
                                </p>
 
                            </form>
 
                        </div>
                    </div>
                </div>
            </div>
        </div>
    )
}
 
export default Signup

 

 

백엔드 : Spring Boot 설정 파일

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package com.example.springjwt.config;
 
 
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
@Configuration
public class CorsMvcConfig implements WebMvcConfigurer {
 
    @Override
    public void addCorsMappings(CorsRegistry corsRegistry) {
        corsRegistry.addMapping("/**")
                .allowedOrigins(
                        "https://spb.abc.com",
                        "http://localhost:3000"
                ) // 정확한 출처 지정
                .allowedMethods("GET""POST""PUT""DELETE""OPTIONS"// 허용할 메서드 명확히 설정
                .allowCredentials(true// 인증 정보 포함 허용
                .allowedHeaders("Authorization""Content-Type""Cache-Control");
    }
}
 
/**
 * 이 설정을 유지하려면 SecurityConfig.java에서 .cors() 설정을 제거해야 한다.
 * (권장) WebMvcConfigurer를 삭제하고, SecurityConfig.java에서 CORS를 관리하는 것이 좋다.
 */
 
package com.example.springjwt.config;
 
import com.example.springjwt.jwt.CustomLogoutFilter;
import com.example.springjwt.jwt.JWTFilter;
import com.example.springjwt.jwt.JWTUtil;
import com.example.springjwt.jwt.LoginFilter;
import com.example.springjwt.repository.RefreshRepository;
import com.example.springjwt.service.RSAService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
 
import java.util.Arrays;
import java.util.Collections;
 
@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    // AuthenticationManager 가 인자로 받을 AuthenticationConfiguraion 객체 생성자 주입
    private final AuthenticationConfiguration authenticationConfiguration;
    private final JWTUtil jwtUtil;
    private final RefreshRepository refreshRepository;
    private final RSAService rsaService;
 
    public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil, RefreshRepository refreshRepository, RSAService rsaService) {
 
        this.authenticationConfiguration = authenticationConfiguration;
        this.jwtUtil = jwtUtil;
        this.refreshRepository = refreshRepository;
        this.rsaService = rsaService;
    }
 
    //AuthenticationManager Bean 등록
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
 
        return configuration.getAuthenticationManager();
    }
 
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 
        http
                .cors((corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
 
                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
 
                        CorsConfiguration configuration = new CorsConfiguration();
                        configuration.setAllowedOriginPatterns(Arrays.asList(
                                "https://spb.abc.com",
                                "http://localhost:3000"
                        )); // allowedOrigins("*")을 사용하지 않고, 구체적인 출처를 명시
                        configuration.setAllowCredentials(true);
 
                        // allowedMethods("*") 대신 GET, POST, PUT, DELETE, OPTIONS를 명확하게 설정
                        // configuration.setAllowedMethods(Collections.singletonList("*"));
                        configuration.setAllowedMethods(Arrays.asList("GET""POST""PUT""DELETE""OPTIONS"));
 
                        // allowedHeaders("*") 대신 Authorization, Content-Type, Cache-Control을 지정
                        // configuration.setAllowedHeaders(Collections.singletonList("*"));
                        configuration.setAllowedHeaders(Arrays.asList("Authorization""Content-Type""Cache-Control"));
 
                        configuration.setExposedHeaders(Collections.singletonList("Authorization"));
 
                        configuration.setMaxAge(3600L);
 
                        return configuration;
                    }
                })));
 
        //csrf disable
        http
                .csrf((auth) -> auth.disable());
 
        //From 로그인 방식 disable
        http
                .formLogin((auth) -> auth.disable());
 
        //http basic 인증 방식 disable
        http
                .httpBasic((auth) -> auth.disable());
 
 
        //경로별 인가 작업
        http
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/api/join""/login""/""/api/public-key""/api/pub-key").permitAll()
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .requestMatchers("/reissue").permitAll()
                        .anyRequest().authenticated());
 
        http
                .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);
 
        http
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshRepository, rsaService), UsernamePasswordAuthenticationFilter.class);
 
        http
                .addFilterBefore(new CustomLogoutFilter(jwtUtil, refreshRepository), LogoutFilter.class);
        
        //세션 설정
        http
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));
 
        http
                .csrf((auth) -> auth.disable());
 
        return http.build();
    }
}
 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.example.springjwt.controller;
 
import com.example.springjwt.dto.JoinDTO;
import com.example.springjwt.service.JoinService;
import com.example.springjwt.service.RSAService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
 
import java.util.Map;
import java.util.UUID;
 
@RestController
@RequestMapping("/api")
public class AuthController {
 
    private final RSAService rsaService;
    private final JoinService joinService;
 
    @Autowired
    public AuthController(RSAService rsaService, JoinService joinService) {
        this.rsaService = rsaService;
        this.joinService = joinService;
    }
 
    /**
     * 회원 가입용 공개키
     */
//    @GetMapping("/pub-key")
//    public Map<String, String> getPubKey() {
//        return Map.of("publicKey", rsaService.getPubKey());
//    }
 
    @GetMapping("/pub-key")
    public Map<StringString> getPubKey(HttpServletRequest request) {
        String clientIp = getClientIp(request);
        int clientPort = request.getRemotePort(); // ✅ 클라이언트 포트 가져오기
        System.out.println("📌 클라이언트 IP: " + clientIp + " | 포트: " + clientPort);
        return Map.of("publicKey", rsaService.getPubKey());
    }
 
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
 
 
    /**
     * 로그인 용 공개키
     */
    @GetMapping("/public-key")
    public Map<StringString> getPublicKey() {
        String sessionId = UUID.randomUUID().toString(); // 클라이언트마다 새로운 세션 ID 생성
        String publicKey = rsaService.generateNewPublicKey(sessionId);
        return Map.of("sessionId", sessionId, "publicKey", publicKey);
    }
 
 
    @PostMapping("/join")
    public Map<StringString> joinProcess(@RequestBody JoinDTO joinDTO) {
        joinService.joinProcess(joinDTO, rsaService.getPriKey());
        return Map.of("message""회원가입 성공");
    }
 
}
 
 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

Windows 11 노트북에서 접속할 때는 인증서가 필요없이도 잘 접속이 되는데 도메인 URL을 통해 접속하는 리눅스 서버에서 443 포트로 접속하는 통신을 하려면 인증서가 필요하다.

 

# SSL 인증서 로컬 시스템에 등록 (Rocky 9)
PKCS12 (Public Key Cryptograhic Standards)의 줄임말로 패스워드로 보호된 형식으로써, 여러 인증서 및 키를 포함할 수 있다. Java뿐만 아니라 여러 플랫폼에서 사용 가능하다.
Letsencrypt 에서 얻는 키를 이용하여 생성하면 된다.

# PKC12생성하기 : Java KeyStore (keystore.p12)로 변환
SSL 인증서를 Spring Boot에 추가하여 HTTPS를 8080 포트에서 활성화하려면, 다음 단계를 따라 Java KeyStore (JKS) 또는 PKCS12 포맷으로 변환하고 설정해야 한다.


openssl pkcs12 -export -in cert1.pem -inkey privkey1.pem -out keystore.p12 -name tomcat -password pass:changeit

 


-export: PKCS12 파일을 생성
-in cert1.pem  → 공개 인증서 파일
-inkey privkey1.pem 개인 키 파일
-out keystore.p12 변환된 인증서 저장 위치
-name tomcat 별칭(alias) 설정
-password pass:changeit  비밀번호 설정 (Spring Boot 설정에서 동일하게 사용해야 함)

생성된 인증서를 위의 3번과 같이 Windows 11 환경의 IntelliJ IDEA 구동한 

spring boot 프로젝트의 resources 폴더에 넣고 build 해서 생성된 파일을 리눅스 시스템에 업로드하면 된다.

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

Spring Boot Security 를 적용한 예시 화면이다.

로그인을 누르면 공개키 정보를 서버에서 가져오는 걸 콘솔화면에 출력되도록 설정하고 확인하는 과정이다.

 

 

로그인 정보를 입력하고 이후 결과 화면을 보면

 

아이디 정보와 비밀번호가 RSA 암호화된 것을 확인할 수 있다.

sessionId 는 RSA 암호화/복호화 KEY 쌍을 사용자별로 일정시간동안만 유지될 수 있도록 구현했다.

로그인이 정상적으로 잘 되면 accessToken 정보를 반환하도록 테스트 했다.

 

JWT 토큰에 대한 강좌는

https://www.youtube.com/watch?v=NPRh2v7PTZg&list=PLJkjrxxiBSFCcOjy0AAVGNtIa08VLk1EJ

 

강좌 시리즈가 설명이 너무 잘 되어 있어서 이 동영상 강좌를 따라서 연습했다.

이 동영상 강좌는 Front-End 에 대한 부분은 POSTMan Web 사이트를 이용하여 테스트를 하기 때문에 실제 환경 구성과는 거리가 있을 수 있다.

 

Windows 11 환경에서 인털리전스 IDEA 툴과 VSCode 로 Spring Boot 예제와 React 예제를 만들어서 구현 테스트 하는 것까지는 어렵지 않게 구현하는데 성공했다.

 

클라우드 서버에 올리는 걸 테스트 하면서 삽질을 엄청나게 했다.

React http://localhost:3000, Spring Boot http://localhost:8080 으로 기본 설정되기 때문에 윈도우에서 둘 간에 어려움없이 개발 테스트를 할 수 있다.

하지만 Rocky 9.5 Linux 서버가 있는 환경에서 구동 테스트를 시도했더니 엄청난 난관에 부딪치게 된다.

문제에 부딪치면 밤을 세워서라도 해결하고픈 욕망 때문에 어젯밤을 꼴딱 샜다. 그리고 해결하고 정리를 하면서 기록을 해두고 있다. 지금 이 순간에는 알지만, 시간이 지나면 금방 잊어버리게 된다.

기록된 결과는 이 글을 보는 방문자에게도 도움되지만, 나에게 가장 큰 도움이 된다.

 

JMeter 로 로그인 처리하는 방법을 배우면서 토큰 인증에 대한 이해가 필요했고, 토큰 인증에 대한 이해를 위해서 샘플 코드를 직접 구현하고 실 사이트에서 테스트해보고, 다시 Spring Boot 환경 구성을 해보면서 결과를 얻으려고 한다.

지금 정리하는 것은 Spring Boot 를 단계적으로 배워서 정리하는 것이 아니라 골인 지점에 대한 구현을 단기 속성으로 하고 나서 거꾸로 Spring Boot 동영상 강좌를 듣고 있는 중이다.

 

PHP 로 구현한 Secure Coding 코드 기반으로 보안 검증을 몇년간 받아보면서 시큐어코딩에 대한 이해도가 높아졌고, 그걸 기반으로 React 와 Spring Boot 기반의 RSA 암호화/복호화 코드를 구현 테스트 했다.

Javascript 에서 RSA 암호화로 패스워드(password)를 암호화해서 전송하고, 서버단에서 RSA 암호화 문자열을 복호화하여 DB에 있는 정보와 일치 여부를 확인한다.

이처럼 이종 언어간에 RSA 암호화/복호화하는 방법으로 결과가 성공이어야 한다.

구글링으로 구한 RSA 암호화/복호화 샘플이 이종 언어간에 제대로 동작되지 않는 걸 많이 경험한다.

Andorid 앱에서 RSA 암호화를 하고, Back-End 언어어서 RSA 복호화 처리를 하는 걸 구현해야만 보안 검증에 통과되기 때문에 이때도 수많은 삽질을 했었고, 이종 언어간에 동작하는 함수를 구현해서 사용했었다.

 

chatGPT 가 나날이 발전하여 많은 도움을 받고 있지만, 잘못된 정보를 전달하기도 해서 시행착오를 많이 겪는다.

Android APP 에서 구현해서 사용했던 함수를 Spring Boot 용으로 변환 요청을 했더니 엉터리로 결과를 반환한다.

이종 언어로 결과 반환을 요청하면 대부분 실패된 결과를 제공하더라.

728x90
블로그 이미지

Link2Me

,
728x90

인텔리전트 IDEA 툴에서 빌드하는 방법이다.

 

빌드된 파일은 아래 경로에서 확인할 수 있다.

 

윈도우에서 빌드한 파일을 리눅스 서버(Rocky Linux 9.5)에 업로드 한다.

 

빌드된 파일은 java -jar 빌드파일명.jar 명령어를 입력하여 실행할 수 있다.

 

구동중에 에러가 발생하면 메시지가 출력되며, 출력된 메시지를 분석해서 에러 원인을 제거해 나가면 된다.

 

728x90

'Spring Boot > IDEA' 카테고리의 다른 글

IntelliJ IDEA Auto Import 설정  (0) 2025.03.01
Build Gradle Update  (0) 2025.02.27
IntelliJ IDEA 에서 Spring 프로젝트 생성  (0) 2021.10.20
블로그 이미지

Link2Me

,
728x90

아래 함수는 Javascript 에서 암호화된 문자열을 PHP 언어에서 복호화 성공한 암호화 KEY 문자열과 복호화 KEY 문자열을 가지고 테스트 진행했다. 함수가 제대로 동작되는 걸 확인했다.

Android Java 코드로 RSA 암호화/복호화 함수 구현 것을 chatGPT에게 변환 요청한 것은 성공되지 못하고 실패했다.

chatGPT가 이기종 언어에서 구현한 코드를 변환 요청하면 대부분 실패하는 거 같다.

Front-End 언어에서 암호화하고 Spring Boot 에서 복호화가 정상적으로 되어야 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package com.example.springjwt.util;
 
import org.springframework.stereotype.Component;
 
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
 
@Component
public class RSAUtil {
 
    /**
     * 2048비트 RSA 키 쌍 생성
     */
    public KeyPair generateRSAKeyPair() {
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            return keyPairGenerator.generateKeyPair();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Failed to generate RSA key pair", e);
        }
    }
 
    /**
     * Base64 인코딩된 공개키 문자열을 PublicKey 객체로 변환 (X.509)
     */
    public PublicKey getPublicKeyFromBase64(String base64PublicKey) {
        try {
            byte[] decodedKey = Base64.getDecoder().decode(base64PublicKey);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
        } catch (Exception e) {
            throw new RuntimeException("Failed to retrieve public key from Base64", e);
        }
    }
 
    /**
     * Base64 인코딩된 개인키 문자열을 PrivateKey 객체로 변환 (PKCS8)
     */
    public PrivateKey getPrivateKeyFromBase64(String base64PrivateKey) {
        try {
            byte[] decodedKey = Base64.getDecoder().decode(base64PrivateKey);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
        } catch (Exception e) {
            throw new RuntimeException("Failed to retrieve private key from Base64", e);
        }
    }
 
    /**
     * Base64 인코딩된 공개키 문자열을 받아 RSA 암호화 수행 (RSA/ECB/PKCS1Padding 사용)
     */
    public String encryptRSA(String plainText, String base64PublicKey) {
        try {
            PublicKey publicKey = getPublicKeyFromBase64(base64PublicKey);
            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encryptedBytes);
        } catch (Exception e) {
            throw new RuntimeException("RSA encryption failed", e);
        }
    }
 
    /**
     * Base64 인코딩된 개인키 문자열을 받아 RSA 복호화 수행 (RSA/ECB/PKCS1Padding 사용)
     */
    public String decryptRSA(String encryptedText, String base64PrivateKey) {
        try {
            PrivateKey privateKey = getPrivateKeyFromBase64(base64PrivateKey);
            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
            return new String(decryptedBytes, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("RSA decryption failed", e);
        }
    }
 
}
 

 

 

 

테스트 하는 방법은 아래와 같이 파일을 추가해서 하면 된다.

RSA Key 쌍을 생성하지 않아도 된다. RSA Key 쌍을 Linux 시스템에서 파일로 생성한 것으로 테스트를 진행했고 잘 동작되는 걸 확인했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
 
package com.example.springjwt.util;
 
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
 
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
 
@Component
public class RSAUtilRunner implements CommandLineRunner {
 
    private final RSAUtil rsaUtil;
 
    public RSAUtilRunner(RSAUtil rsaUtil) {
        this.rsaUtil = rsaUtil;
    }
 
    @Override
    public void run(String... args) throws Exception {
        // 1. RSA 키 쌍 생성
        KeyPair keyPair = rsaUtil.generateRSAKeyPair();
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();
 
        // 2. Base64로 인코딩하여 키 출력
        String base64PublicKey = java.util.Base64.getEncoder().encodeToString(publicKey.getEncoded());
        String base64PrivateKey = java.util.Base64.getEncoder().encodeToString(privateKey.getEncoded());
 
        System.out.println("----- RSA 키 쌍 생성 완료 -----");
        System.out.println("공개키 (Base64): " + base64PublicKey);
        System.out.println("개인키 (Base64): " + base64PrivateKey);
 
        // 3. 암호화 테스트
        String originalText = "Hello, Spring Boot with RSA!";
        System.out.println("\n원본 텍스트: " + originalText);
 
        String encryptedText = rsaUtil.encryptRSA(originalText, base64PublicKey);
        System.out.println("암호화된 텍스트: " + encryptedText);
 
        // 4. 복호화 테스트
        String decryptedText = rsaUtil.decryptRSA(encryptedText, base64PrivateKey);
        System.out.println("복호화된 텍스트: " + decryptedText);
 
    }
 
}
 

 

 

다음에는 실제 React 에서 비밀번호를 암호화해서 전송하고, Spring Boot 에서 복호화 성공 여부를 확인하려고 한다.

728x90
블로그 이미지

Link2Me

,
728x90

강동구청 조직도 크롤링 소스코드이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import requests
from bs4 import BeautifulSoup
import pandas as pd
import re
 
def crawl_orgchart():
    url = "https://www.gangdong.go.kr/web/newportal/empSearch/list"
    headers = {"User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
    
    response = requests.get(url, headers=headers)
    if response.status_code != 200:
        print("Failed to retrieve page")
        return None
    
    soup = BeautifulSoup(response.text, "html.parser")
    
    data = []
    
    departments = soup.select("h4.title-green")
    tables = soup.select("table")
    
    for dept, table in zip(departments, tables):
        department_name = dept.text.strip()
        
        # 괄호가 포함되어 있으면 괄호 이전 글자만 추출
        if "(" in department_name:
            department_name = re.split("\\(", department_name)[0].strip()
        else:
            # 'FAX' 이전까지의 글자만 추출
            department_name = re.split("FAX", department_name, flags=re.IGNORECASE)[0].strip()
        
        table_rows = table.select("tbody tr")
        
        for row in table_rows:
            columns = row.find_all("td")
            if len(columns) >= 3:
                position = columns[0].text.strip()
                phone = columns[1].text.strip()
                duty = columns[2].text.strip()
 
                # 전화번호가 없는 행은 제외
                if not phone:
                    continue
 
                # 전화번호가 2줄 이상이면 한 칸 공백으로 띄우고 한 줄로 변환
                phone = " ".join(phone.splitlines()).strip()
                
                # 여러 줄인 경우 한 칸 공백으로 띄우고 한 줄로 변환
                duty = " ".join(duty.splitlines()).strip()
                
                # 담당업무가 1줄이고 첫 글자가 '-'이면 'o'로 변경
                if duty.startswith("-"and "\n" not in duty:
                    duty = "o" + duty[1:]
                
                data.append({
                    "부서명": department_name,
                    "직위": position,
                    "전화번호": phone,
                    "담당업무": duty
                })
    
    df = pd.DataFrame(data)
    return df
 
if __name__ == "__main__":
    df = crawl_orgchart()
    if df is not None:
        print(df)
        df.to_csv("강동구청_조직도.csv", index=False, encoding="utf-8-sig")
 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

종로구청 조직도 크롤링 소스코드이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import requests
from bs4 import BeautifulSoup
import pandas as pd
import re
 
def crawl_orgchart_main(url):
    # 요청 및 응답 확인
    headers = {
        "User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
    }
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.text, "html.parser")
 
    # 데이터 저장 리스트
    data = []
 
    # "org_list" 내부 정보 추출
    org_list = soup.select(".org_list ul li a")
    
    for org in org_list:
        name = org.find("em").get_text(strip=Trueif org.find("em"else ""
        name = re.split("\\d", name)[0].strip()  # 숫자 제거하여 이름만 추출
        duty = org.find("span").get_text(strip=Trueif org.find("span"else ""
        phone = org.find("p").get_text(strip=Trueif org.find("p"else ""
        position = ""
        if name and duty:
            data.append([duty, position,phone, name])
    
    # "modal-table" 내부 정보 추출 (부구청장, 비서실 등)
    modal_tables = soup.select(".modal-table tbody tr")
    for row in modal_tables:
        cols = row.find_all("td")
        if len(cols) >= 3:
            name = cols[0].get_text(strip=True)  # 이름 => 부서명
            duty = cols[1].get_text(strip=True)  # 담당업무
            phone = cols[2].get_text(strip=True)  # 연락처
            position = ""
            data.append([duty, position,phone, name])
    
    # 데이터프레임 생성
    columns = ["부서명""직위""전화번호","담당업무"]
    df = pd.DataFrame(data, columns=columns)
    return df
 
 
def crawl_orgchart(url):
 
    # 요청 및 응답 확인
    headers = {
        "User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
    }
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.text, "html.parser")
 
    # 데이터 저장 리스트
    data = []
 
    # "organization" 내부 정보 추출
    organization= soup.select_one(".organization ul li")
 
    if organization:
        department = organization.find("dt").get_text(strip=Trueif organization.find("dt"else ""
        name_position_parts = organization.find("dd").get_text(strip=True, separator=" ").split(" "if organization.find("dd"else []
        name_position = name_position_parts[1if len(name_position_parts) > 1 else name_position_parts[0if name_position_parts else ""        
        # name_position = organization.find("dd").get_text(strip=True, separator=" ").split(" ")[1] if organization.find("dd") else ""
        phone_tag = organization.select_one(".telnum a")
        phone = phone_tag.get_text(strip=Trueif phone_tag else ""
        duty_position_parts = organization.find("dd").get_text(strip=True, separator=" ").split(" "if organization.find("dd"else []
        duty = " ".join(duty_position_parts[:2]) if len(duty_position_parts) > 1 else duty_position_parts[0if duty_position_parts else ""
        data.append([department, name_position, phone, duty])
 
    departments = soup.select(".text_area.depart-team .mb5 .table_type1 tbody tr th")
    tables = soup.select(".table_type1.mt20")
 
    # 테이블 데이터 추출
    for dept, table in zip(departments, tables):
        department_name = dept.text.strip()
 
        table_rows = table.select("tbody tr")
        for row in table_rows:
            columns = row.find_all("td")
            if len(columns) >= 3:
                position = columns[0].get_text(strip=True)  # 직위
                duty = columns[1].get_text(strip=True)  # 담당업무
                phone = columns[2].get_text(strip=True)  # 행정전화번호
                data.append([department_name, position, phone, duty])
 
    columns = ["부서명""직위""전화번호","담당업무"]
    df = pd.DataFrame(data, columns=columns)
    return df
 
if __name__ == "__main__":
    url_main = "https://www.jongno.go.kr/Main.do?menuId=1917&menuNo=1917"
    df_main = crawl_orgchart_main(url_main)
 
    if not df_main.empty:
        print(df_main)
        df_main.to_csv("종로구청_main.csv", index=False, encoding="utf-8-sig")
 
    urls = [
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002110000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30001900000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002340000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002350000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002360000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002430000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002440000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002450000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002460000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002470000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002480000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002490000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002500000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002510000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002520000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002530000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002540000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002710000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002720000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002730000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002740000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002750000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002760000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002770000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002780000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002790000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002550000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002560000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002570000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002580000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002590000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002600000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002800000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002810000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002820000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002830000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002310000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30001530000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30000370000",
        "https://www.jongno.go.kr/portal/deptGuidance.do?menuId=1892&deptId=30002320000",
    ]
    
    # 모든 URL에서 데이터 크롤링
    dataframes = [crawl_orgchart(url) for url in urls]
    
    # 데이터프레임 병합
    df_combined = pd.concat(dataframes, ignore_index=True)
    
    # 두 데이터프레임을 합치기
    if not df_combined.empty:
        print(df_combined)
        df_combined.to_csv("종로구청.csv", index=False, encoding="utf-8-sig")
 

 

크롤링하는 방법에 대한 설명은 생략한다.

 

조직도를 누르면

https://www.jongno.go.kr/Main.do?menuId=1917&menuNo=1917

 

조직도<구청<종로구 안내<종로소개< 종로구

조직도 ※ 해당 부서/동주민센터를 누르시면 부서별/동주민센터별 홈페이지를 보실 수 있습니다. 7국, 1소, 1사무국, 1담당관, 40과 구청 > 조직도 수정 221017 --> 본 저작물은 공공누리 「제 1유형:

www.jongno.go.kr

조직도 사이트가 나오는데 이용자 중심으로 되어 있다보니 크롤링하기가 좀 번거롭다.

 

크롤링 하는데 빠진 자료도 있을 수 있다.

 

728x90
블로그 이미지

Link2Me

,
728x90

인텔리제이 IDEA 사용 시 Auto Import 기능은 반드시 설정해야 코딩시 편하다.

Import 해야 할 대상이 2개 이상인 경우를 제외하고, 1개인 것은 자동으로 모두 추가/제거를 해준다.

 

728x90

'Spring Boot > IDEA' 카테고리의 다른 글

Spring Boot Build 하기  (0) 2025.03.08
Build Gradle Update  (0) 2025.02.27
IntelliJ IDEA 에서 Spring 프로젝트 생성  (0) 2021.10.20
블로그 이미지

Link2Me

,