Posted by 세상을 살아가는 사람
,

##c%% 이세상 소풍 끝내는 날
 가서 아름다웠더라고 말하리라.##e%%1

---------------------------------

##s%%0##t%%귀천##a%%천상병##c%%나 하늘로 돌아가리라
 새벽빛 와 닿으면 스러지는
 이슬 더불어 손잡고
 
 나 하늘로 돌아가리라
 노을빛 함께 단둘이서
 기슭에서 놀다가
 구름 손짓하면은
 
 나 하늘로 돌아가리라
 아름다운##n%%1

------------------------------

##c%%끄러움 가득 머금은 벚꽃에 
봄 날에 그렇게 빠져들었나 보다
봄에 빠진 우리들이 하나 둘씩 
분홍색 부끄러움 한 잎씩 떨어트려
저 벚나무를 피워냈나 보다.##e%%1

------------------------

##s%%1##t%%벛꽃은 그렇게 피더라##a%%김인성##c%%봄 바람 산뜻 불어오는 길목에 
벚꽃이 순하게 피어있다.
무엇이 그렇게 부끄러운지 
붉게 물들인 채로
하루는 꽃 내음에
하루는 나비의 날개짓에
하루는 못 다핀 한 떨기의 꽃에
부##n%%1

-------------------

##c%%지 않는다

슬픈 맹약에 서사시도 아닌. 
서글픈 매일의 서두름에
오감의 황홀함이 악세서리처럼
몸에 둘린다.
슬픈 기억만이 전설에 남는것은 아니다.
독특하고 흔하지 않은 삶의 영혼이 
흔한 삶을 무르익게 한다.

##e%%1

-----------------------

##s%%1##t%%추억##a%%k##c%%붉게 물든 서녁에 비친 바다에 
물소리의 기억밖에 나지 않는다.
수면 위에 반짝이는 향기가.
마치 샛별의 숨소리같다.

별은 또 다시 바다를 비춰 
운명을 비추려는 것일까
검은 하늘의 별들이 사라지##n%%1

----------------------------

##c%%관처럼 술잔을 기운다.

가슴 한켠 낡은 사랑은

빗방울 되어 기다림에 스민다.

시리도록 아름다웠던 기억도

일상이 되어버린 슬픔도

어제처럼 오늘도...

아무 일도 아닌 것처럼

빗물 속에 숨는다.

##e%%1

--------------------------

##c%%진 화장을 지운다.

시간이 지나면 괜찮아 질 거라는 말

그런 거짓말 세상에 없더라...

어제처럼 오늘도

아무 일도 아닌 것처럼

흐린 거울 속에 숨는다.

바보 같은 남자...

오래된 사진을 지운다.

습##n%%1

---------------------------------

##s%%1##t%%독백##a%%곽명길##c%%제목 : 독백                   

바보 같은 여자...

화장을 지운다.

즐거웠던 기억도, 행복했던 기억도...

언제나 기다려 줄꺼라 믿었던 미소도...

습관처럼 얼룩##n%%1

Posted by 세상을 살아가는 사람
,

이전 게시글에서 Python에서 pymysql module을 사용하여 MySQL 테이블에 데이터를 삽입하는 방법을 확인하였었다.

이번 게시글에서는 테이블에 저장된 데이터를 검색하여 출력해 본다.

import pymysql

# 변수 선언 부분
conn, cursor = None, None
data1, data2, data3, data4 = "", "", "", ""
row = None

# 메인 코드 부분
conn = pymysql.connect(host='localhost', user='study', password='study', db='studydb', charset='utf8')
cursor = conn.cursor()

cursor.execute("select * from userTable")

print("사용자ID    사용자이름    이메일            출생연도")
print("--------------------------------------------------------")

while (True) :
    row = cursor.fetchone()
    if row == None :
        break;
    data1 = row[0]
    data2 = row[1]
    data3 = row[2]
    data4 = row[3]
    print("%7s   %12s   %15s   %5d" % (data1, data2, data3, data4))

conn.commit()
conn.close()

cmd창에서 수행 결과는 아래와 같다. 역시 idle에서는 실행되지 않았다. 아마 sys.path에 들어있지 않아서 그런 것 같다.

'Python' 카테고리의 다른 글

python mysql 연동  (0) 2023.01.17
[프로그램 1] 다이어몬드 출력  (0) 2023.01.14
Posted by 세상을 살아가는 사람
,

python mysql 연동

Python 2023. 1. 17. 16:48

Python에서 MySQL 데이터베이스를 사용하는 방법을 적어본다.

- 환경

  + Python 3.9.10

  + MySQL : 8.0.29

  + anaconda3에서 pymysql 모듈 설치

- idle에서 파일 작성하여 run module 실행하면 pymysql 모듈이 없다고 동작하지 않음

- cmd 창에서 python Code13-02.py 입력하여 실행하면 정상 동작

[테이블 생성 및 데이터 초기화]

테이블 생성을 위해 사용한 SQL문은 아래와 같다.

create table userTable (
	id varchar(10),
    userName varchar(15),
    email varchar(20),
    birthYear int
);

insert into userTable values ('john', 'John Bann', 'john@naver.com', 1990);
insert into userTable values ('kim', 'Kim Chi', 'kim@daum.net', 1992);
insert into userTable values ('lee', 'Lee Pal', 'lee@paran.com', 1988);
insert into userTable values ('park', 'Park Su', 'park@gmail.com', 1980);

select * from userTable;

[userTable에 사용자 정보를 터미널에서 입력으로 받아 저장] : Code13-01.py

import pymysql

# 변수 선언 부분
conn, cursor = None, None
data1, data2, data3, data4 = "", "", "", ""
sql = ""

# 메인 코드 부분
conn = pymysql.connect(host='localhost', user='study', password='study', db='studydb', charset='utf8')
cursor = conn.cursor()

while (True):
    data1 = input("사용자ID ==> ")
    if data1 == "":
        break;
    data2 = input("사용자이름 ==> ")
    data3 = input("이메일 ==> ")
    data4 = input("출생년도 ==> ")
    sql = "insert into userTable values ('" + data1 + "','" + data2 + "','" + data3 + "','" + data4 + "')"
    cursor.execute(sql)
conn.commit()
conn.close()

cmd 창을 사용하여 Code13-01.py를 실행한 결과는 아래와 같다.

MySQL workbench를 사용하여 데이터를 확인하면 다음과 같이 잘 저장됨을 알 수 있다.

 

'Python' 카테고리의 다른 글

python에서 MySQL 연동하여 테이블 검색  (0) 2023.01.17
[프로그램 1] 다이어몬드 출력  (0) 2023.01.14
Posted by 세상을 살아가는 사람
,

우재남씨가 지은 "파이썬 for Beginner"에 나오는 [프로그램 1] 다이어몬드 출력 문제를 풀어본다.

문제를 분석해 본다. "  "(스페이스)를 출력하는 부분과 "*"(다이어몬드)를 출력하는 부분으로 나누어서 다음과 같이 i가 0에서 부터 8까지 변경될 경우 그에 따른 변화를 테이블로 작성해 보았다.

i 스페이스 from 스페이스 to 다이어몬드 from 다이어몬든 to 비고
0 0 0 3 4-i-1 4 4-i 4 4+i  
1 0 0 2 4-i-1 3 4-i 5 4+i  
2 0 0 1 4-i-1 2 4-i 6 4+i  
3 0 0 0 4-i-1 1 4-i 7 4+i  
4 -1 4-i-1 -1 4-i-1 0 4-i 8 4+i  
5 0 0 0 i-4-1 1 -(4-i) 7 12-i  
6 0 0 1 i-4-1 2 -(4-i) 6 12-i  
7 0 0 2 i-4-1 3 -(4-i) 5 12-i  
8 0 0 3 i-4-1 4 -(4-i) 4 12-i  

다이어몬드를 출력하는 특성상 출력하는 부분이 i가 0~4까지는 4-i부터 4+i까지 출력이 되어야 하고, i가 5~8까지는 -(4-i)부터 12-i까지 출력이 되어야 한다.

이를 스프레드 시트를 사용하여 표현하면 다음과 같이 나타낼 수 있다.

위의 그래프는 별을 출력하는 시작과 끝을 나타내는 것이다. 첫번째(i = 0)부터 다섯번째(i = 4)까지는 시작은 -i + 4로 나타낼 수 있고, 끝은 i + 4로 나타낼 수 있다. 또 다섯번째(i = 4)부터 아홉번째(i = 8)까지는 시작은 i - 4이고 끝은 -i + 12이다.

이는 절대값을 사용하면 최소값은 | i - 4 |, 최대값은 - | i - 4 | + 8로 나타낼 수 있다. 이를 그래프를 나타내는 절대값과  이동, 대칭으로 나타내면 다음 그림과 같다.

y = | x |를 나타내는 그래프를 오른쪽으로 4만큼 이동시키면 시작을 나타내는 수식(함수 y = | x - 4 |)이 되고, 이를 x축으로 대칭을 시켜서 y = - | x - 4 |로 변경하고 또 y축으로 8만큼 이동시키면 끝을 나타내는 수식(함수 y = - | x - 4 | + 8이 된다.

따라서 다이어몬드를 출력하는 프로그램은 다음과 같이 프로그램할 수 있다.

먼저 0~8까지 줄을 나타내는 반복문을 사용하고 다시 스페이스를 출력하는 부분과 다이어먼드를 출력하는 부분으로 나눌 수 있으며, 다이어몬드를 출력하는 시작과 끝을 위의 수식을 사용하여 나타내면 된다.

for i in range(0, 9, 1):
    k = abs(i - 4)
    l = - abs(i - 4) + 8
    for j in range(0, l + 1, 1):
        if j >= k and j <= l:
            print('\u2605', end='')
        else:
            print("  ", end='')
    print("")

이를 실행하면 다음과 같이 다이어몬드를 출력하는 것을 알 수 있다.

하나의 문제를 푸는 것이 이렇게 복잡하고, 이를 설명하는 것이 또한 복잡하다. 또 이렇게 설명을 한다고 했을 때 이해를 잘 할 수 있는지도 궁금하다. 조금더 이해가 될 수 있도록 작성을 해 보았다.

'Python' 카테고리의 다른 글

python에서 MySQL 연동하여 테이블 검색  (0) 2023.01.17
python mysql 연동  (0) 2023.01.17
Posted by 세상을 살아가는 사람
,

기존에 스프링으로 추가한 것을 스프링 부트로 구현한 것에 검색 세부조건(신상품, Best, 할인)과 정렬조건을 추가로 하였다.

관리자 화면에서 스타일 숍 상품에 대한 검색과 함께 일반 사용자 화면에서 검색 기능을 합하여 구현을 하였다.

github에 소스코드를 다시 push를 하였다.

https://github.com/KYUNGSUB/springboot-lala-market.git

 

GitHub - KYUNGSUB/springboot-lala-market

Contribute to KYUNGSUB/springboot-lala-market development by creating an account on GitHub.

github.com

세부 기능은

- 주 검색 기능

- 세부 검색 기능

- 신상품, Best, 할인 등에 따른 검색

- 정렬조건으로 최신순, 낮은 가격순, 높은 가격순이 있다.

동작 확인은 http://43.200.20.11/product/list이다

물론 관리자로 로그인을 하여야 한다. (user9/!pw09)

로그인하면 관리자 페이지 http://hostname/aindex로 이동 한 후 메뉴 상품 관리->상품 리스트를 선택하면 된다.

Posted by 세상을 살아가는 사람
,

지난 번에 스프링을 이용해서 이정원씨가 지은 "Do it 웹사이트 기획 입문"의 기획서인 라라마켓 쇼핑몰을 일부 구현을 하였고, 블로그에 기술하였다. (https://blog.naver.com/ksseo63/222750784956 ~ https://blog.naver.com/ksseo63/222770023453)

여기서는 스프링 부트를 이용하여 구현해 본다. 같은 기능을 스프링 부트를 이용해서 빨리 구현을 하다보니 기능을 자세히 검증하지 못하고 주 기능만 구현하였다.

구현한 것을 자세히 기술할 필요가 있지만, 남은 기능을 계속 구현할 필요성도 있어서 고민이다. 그래서 우선 여기서 현재까지 구현한 것을 간단하게 기술하고, 앞으로 남은 기능을 계속 추가해 보고자 한다.

우선 개발환경은 다음과 같다.

- Windows 10 Home 또는 Pro OS

- JDK-11.0.16

- STS4 IDE

- MySQL (AWS RDS상에서 동작

구현해야 할 기능에 대한 설계서는 이정원씨가 지은 "Do it 웹 사이트 기획입문"에서 나온 화면정의서(라라마켓_관리자_화면정의서, 라라마켓_이용자_화면저의서)와 여러 기획자료를 참고하였다.

구현한 기능은 다음 순서에 따라 구현하였다. 이것은 스프링으로 개발했던 것을 기반으로 하다보니 같은 순서를 따랐다.

- 라라마켓 레이아웃 추가

- Security 적용

- 관리자 페이지

- 상품 카테고리 관리

- 상품관리 -> 상품등록

- 상품관리 -> 상품 리스트

- 정책관리 -> 정책

- 정책관리 -> 약관관리

- 관리자기능 -> 배너관리

- 회원가입

소스 코드는 github에 등록을 하였다. (https://github.com/KYUNGSUB/springboot-lala-market)

구현된 것을 아마존 클라우드 AWS EC2를 사용하여 동작 시켜 본다.

EC2는 Linux Ubuntu OS를 사용하기 때문에 Windows 환경과는 약간 차이가 있다. 따라서 다음과 같이 수정해 주어야 한다.

github 원격 레포지토리에 있는 소스 코드를 $ git clone https://github.com/KYUNGSUB/springboot-lala-market.git명령어를 입력하여 clone 한다.

$ cd springboot-lala-market을 사용하여 폴더 내로 이동한다.

src/main/resources/application.properties 파일의 설정 정보를 다음과 같이 수정한다.

# server.port=80    # Linux에서 80 포트를 사용하려면 sudo(슈퍼유저) 권한으로 앱을 실행하여야 한다. 따라서 8080 포트를 사용하도록 주석으로 처리해 준다.

spring.servlet.multipart.location=/home/ubuntu/upload

kr.talanton.upload.path=/home/ubuntu/upload

# port
# server.port=80

# 생략된 부분이 있음...
spring.servlet.multipart.enabled=true
#spring.servlet.multipart.location=D:\\zzz\\upload
spring.servlet.multipart.location=/home/ununtu/upload
spring.servlet.multipart.max-request-size=30MB
spring.servlet.multipart.max-file-size=10MB

#kr.talanton.upload.path=D:\\zzz\\upload
kr.talanton.upload.path=/home/ubuntu/upload

# 생략된 부분이 있음...

파일 업로드 시 임시로 생성되는 파일과 파일을 저장할 폴더로 /home/ubuntu/upload를 지정하고 폴더를 생성해 준다.

gradlew 파일에 실행 권한을 부여한다.

$ chmod +x ./gradlew

앱의 실행파일을 생성한다.

$ ./gradlew bootJar./

성공적으로 build가 되고 build/libs 폴더 밑에 springboot-lala-market-0.0.1-SNAPSHOT.jar가 생성됨을 알 수 있다.

생성된 jar 파일을 홈으로 복사한 후 다음 명령어를 사용하여 실행한다.

$ java -jar springboot-lala-market-0.0.1-SNAPSHOT.jar &

AWS EC2는 Ubuntu Linux OS를 사용하므로 Windows OS에서 동작할 때와 약간 차이가 있다. 따라서 일부 수정이 필요하다.

1. basic.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:fragment="setContent(content)">
        <th:block th:replace="~{./layout/header :: header}">
        </th:block>
    <div class="container-fluid">
        <th:block th:replace = "${content}"></th:block>
    </div>
    <th:block th:replace="~{./layout/footer :: footer}">
    </th:block>
</th:block>
</html>

windows OS에서는 <th:block th:replace="~{/layout/header :: header}"> 라고 하여도 동작하나 "./layout"으로 수정을 해주어야 한다. 이하 다른 파일도 동일하다.

2. HomeController.java

package kr.talanton.lala.main.controller;

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

import lombok.extern.log4j.Log4j2;

@Controller
@Log4j2
public class HomeController {
        @GetMapping("/")
        public String home(Model model) {
                log.info("home...");
                return "home";
        }
}

home() 메소드의 반환형으로 view 페이지를 지정하기 위하여 Windows OS에서는 "/home"을 사용하나, AWS EC2에서는 "home"을 사용하여야 한다. 다른 Controller일 경우도 이와 같이 처리해 주어야 한다.

아래는 브라우저에서 AWS EC2의 주소를 사용하여 접속한 경우이다.

다음과 같이 iptables을 설정하면 80 port 번호로 들어오는 패킷을 8080 포트로 port forwarding할 수 있으며, 외부에서 80 port로 접속할 수 있다.

$ sudo iptables -A PREROUTING -t nat -p tcp --dport 80 -j REDIRECT --to-ports 8080

Posted by 세상을 살아가는 사람
,

이제 2주면 현재 강의하고 있는 과정이 끝난다.

11명이 남았는데, 3명이 취직을 해서 나오지 않고 있다.

한 명은 취직을 위해 오늘도 면접을 본다.

나머지 7명

그중 휴가 한 명(?)

3명은 나와서 포트폴리오 작업 중

한 명은 지각한다고(?)

한 명은 연락 두절(?)

한 명은 또 지각(?)

고지가 이제 얼마 남지 않았는데, 꾸물거릴 시간이 없다.

꾸준히 노력을 해서 더 좋은 직장, 더 나은 대접을 받아야겠다.

나는 그렇게 생각하는데, 요즘 젊은이들 이해가 안된다...

내가 재벌집 막내 손자로 다시 태어나도 그럴까?

나는 후회없는 하루를 보내고 싶다.

나는 한번 경험을 해서 안다. 지금은 꾸물거릴 때가 아니다.

꾸준히 최선을 다해서 공부를 해야할 때이다.

그래야 인정을 받는다...

동정을 받지 말고, 비굴하지 말고, 한심한 존재로 평가받지 말아야 한다...

 

'old_인생사' 카테고리의 다른 글

OOP  (0) 2015.07.27
기술의 발전에 따른 교육의 어려움  (0) 2015.07.25
속이 보일땐  (0) 2015.07.17
티스토리를 다시 시작하면서  (0) 2015.07.12
Posted by 세상을 살아가는 사람
,

김은옥씨가 지은 은노기의 JSP2.3 웹프로그래밍에 나오는 shoppingmall 프로그램을 돌려보기 위한 참고사항을 적어본다.

소스코드는 아래에 첨부한다.

shoppingmall.zip
3.43MB

이 프로그램은 데이터베이스로 MySQL을 사용하였다. 나는 v8.0.13을 사용하였다.

위 소스코드 중 WebContent/WEB-INF/sql/database.sql 파일을 참조한다.

다음과 같이 데이터베이스 및 사용자를 생성하고 권한을 부여해준다. (root 권한으로 실행한다.)

-- 은노기 shoppingmall 프로젝트
create database jsptest;
create user 'jspid'@'localhost' identified by 'jsppass';
create user 'jspid'@'%' identified by 'jsppass';
grant all privileges on jsptest.* to 'jspid'@'localhost';
grant all privileges on jsptest.* to 'jspid'@'%';
commit;

다음과 같이 테이블과 사용자 데이터를 추가한다.

create table member(
  id varchar(50) not null primary key,
  passwd varchar(16) not null,
  name varchar(10) not null,
  reg_date datetime not null
);

alter table member
     add (address varchar(100) not null,
          tel varchar(20) not null);
          
desc member;

insert into member(id, passwd, name, reg_date, address, tel) 
values('kingdora@dragon.com','1234','김개동', now(), '서울시', '010-1111-1111');

insert into member(id, passwd, name, reg_date, address, tel) 
values('hongkd@aaa.com','1111','홍길동', now(), '경기도', '010-2222-2222');

select * from member;

alter table member modify passwd varchar(60) not null;

create table board(
  num int not null primary key auto_increment,
  writer varchar(50) not null,
  subject varchar(50) not null,
  content text not null,
  passwd varchar(60) not null,
  reg_date datetime not null,
  ip varchar(30) not null,
  readcount int default 0,
  ref int not null,
  re_step smallint not null,
  re_level smallint not null
);

desc board;

--쇼핑몰

create table manager(
 managerId varchar(50) not null primary key,
 managerPasswd varchar(60) not null
);

insert into manager(managerId,managerPasswd)
values('bookmaster@shop.com','123456');

insert into manager(managerId,managerPasswd)
values('ksseo63@naver.com','ekffksxm0');

create table book(
  book_id int not null primary key auto_increment,
  book_kind varchar(3) not null,
  book_title varchar(100) not null,
  book_price int not null,
  book_count smallint not null,
  author varchar(40) not null,
  publishing_com varchar(30) not null,
  publishing_date varchar(15) not null,
  book_image varchar(16) default 'nothing.jpg',
  book_content text not null,
  discount_rate tinyint default 10,
  reg_date datetime not null
);

create table qna(
  qna_id int not null primary key auto_increment,
  book_id int not null,
  book_title varchar(100) not null,
  qna_writer varchar(50) not null,
  qna_content text not null,
  group_id int not null,
  qora tinyint not null,
  reply tinyint default 0,
  reg_date datetime not null
);

create table bank(
  account varchar(30) not null,
  bank varchar(10) not null,
  name varchar(10) not null
);

insert into bank(account, bank, name)
values('11111-111-11111','내일은행','오내일');

create table cart(
  cart_id int not null primary key auto_increment,
  buyer varchar(50) not null,
  book_id int not null,
  book_title varchar(100) not null,
  buy_price int not null,
  buy_count tinyint not null,
  book_image varchar(16) default 'nothing.jpg'
);

create table buy(
  buy_id bigint not null,
  buyer varchar(50) not null,
  book_id varchar(12) not null,
  book_title varchar(100) not null,
  buy_price int not null,
  buy_count tinyint not null,
  book_image varchar(16) default 'nothing.jpg',
  buy_date datetime not null,
  account varchar(50) not null,
  deliveryName varchar(10) not null,
  deliveryTel varchar(20) not null,
  deliveryAddress varchar(100) not null,
  sanction varchar(10) default '상품준비중'
);

데이터베이스 연동을 위한 설정은 WebContent/META-INF/context.xml을 참조한다.

<?xml version="1.0" encoding="UTF-8"?>
<Context>
	<Resource auth="Container" driverClassName="com.mysql.jdbc.Driver" maxWait="5000" name="jdbc/eun" password="jsppass" type="javax.sql.DataSource" url="jdbc:mysql://localhost:3306/jsptest?useSSL=false" username="jspid"/>
</Context>

또한 MySQL JDBC Connector 파일도 자신이 사용하는 MySQL Server 버전에 맞추어 변경해 주어야 한다. 이것은 studyjsp와 shoppingmall 프로젝트의 WebContent/WEB-INF/lib 폴더 밑에 있는 JDBC Connecctor 라이브러리에 대하여 맞추어 주어야 한다. 나는 v8.0.13을 사용하므로 mysql-connector-java-8.0.13.jar로 변경해 주었다. (기존에는 5.x.x 버전을 사용)

사용자에 대한 암호를 암호화하기 위해서는 9장에 있는 cryptProcess.jsp를 구동해 주어야 한다. (http://localhost:8080/shoppingmall/ch09/cryptProcess.jsp)

이는 studyjsp 프로젝트 폴더에 있는 소스코드를 shoppingmall 프로젝트 폴더로 복사하여 사용하였다.

은노기의 JSP2.3 웹프로그래밍 9장 375p에 자세한 설명이 되어 있다.

다음은 ch09/cryptProcess.jsp 파일에 대한 내용이다.

<%@page import="ch09.update.UpdateDBBean"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import = "java.util.List" %>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<link rel="stylesheet" href="style.css"/>

<h3>암호화 전 내용</h3>
<jsp:include page="cryptProcessList.jsp" flush="false"/>

<%
  UpdateDBBean dbPro = UpdateDBBean.getInstance();
  dbPro.updateMember();
%>

<h3>암호화가 적용된 후 내용</h3>
<jsp:include page="cryptProcessList.jsp" flush="false"/>

또한 관리자 계정의 비밀번호도 암호화를 수행하여야 하며, 브라우저에서http://localhost:8080/shoppingmall/enc/cryptProcess.jsp를 수행하면 된다.

cryptProcess.jsp에 대한 소스 코드는 아래와 같다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import = "java.util.List" %>
<%@ page import = "mngr.enc.PassCrypt" %>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>

<%
  PassCrypt dbPro = PassCrypt.getInstance();
  dbPro.cryptProcess();
  out.println("암호화 성공! 꼭 한번만 수행");
%>
 

소스코드에 대한 동작을 확인하기 위해서는 관리자로 로그인하여 책에 대한 상품등록을 카테고리별로 3권씩 등록을 해주면 좋을 것 같다. (인터파크 등을 참고하면 쉽게 할 수 있음)

Posted by 세상을 살아가는 사람
,

이전 게시글에서는 로그인 여부에 따른 메뉴 표시와 로그아웃 기능을 구현하였다.(https://talanton.tistory.com/128)

이 글에서는 회원가입 기능을 구현하고자 한다.

- 회원가입 폼을 작성

- 아이디 중복 확인

- 회원가입 절차

초기 동작은 오른쪽 위의 Dropdown 메뉴에서 Join 메뉴를 클릭하면 /member/join이 서버로 요청이 되며, 회원가입 폼이 보여져야 한다.

서버에서는 /member/join url 요청을 처리하기 위하여 MemberController가 동작한다.

	@GetMapping("/join")
	public void join() {
		log.info("join...");
	}

뷰 페이지는 templates/member/join.html이 동작한다.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">
    <th:block th:fragment="content">
		<script src="/js/join.js"></script>
		
        <h1 class="mt-4">Join Page</h1>
        <form class="form-join mt-4" method="post" action="/member/join">
        	<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
        	<input type="hidden" name="fromSocial" value="false">
			<div class="form-row">
				<div class="col-lg-10">
					<label>아이디(이메일 형식) *</label>
					<input class="form-control" placeholder="kdhong@naver.com" name="email" type="email" required autofocus>
				</div>
				<div class="col-lg-2">
					<label>* : 필수입력</label>
					<button type="button" class="form-control btn btn-secondary">중복확인</button>
				</div>
			</div>
			<div class="form-row mt-3">
				<div class="col-lg-6">
					<label>암 &nbsp; 호 *</label>
					<input class="form-control"	placeholder="암호 입력 (특수문자, 영문, 숫자의 조합으로 8자 이상 15자 이하)" name="password" type="password" required>
				</div>
				<div class="col-lg-6">
					<label>암호 확인 *</label>
					<input class="form-control" placeholder="암호 입력 (특수문자, 영문, 숫자의 조합으로 8자 이상 15자 이하)" name="password2" type="password" required>
				</div>
			</div>
			<div class="form-row mt-3">
				<div class="col-lg-6">
					<label>이 &nbsp; 름 *</label>
					<input class="form-control"	placeholder="홍길동" name="name" type="text" maxlength="30" required>
				</div>
				<div class="col-lg-6">
					<label>별 &nbsp; 명</label>
					<input class="form-control" placeholder="바람의 아들" name="nickname" maxlength="30" type="text">
				</div>
			</div>
			<div class="form-row mt-3">
				<label>휴대전화</label>
				<input class="form-control" placeholder="010-3333-4444(- 포함)" name="phone" type="tel">
			</div>
			<div class="form-group row mt-3">
				<label for="colFormLabel" class="col-lg-1 col-form-label">생년월일 : </label>
				<div class="col-lg-1">
					<select name="calendar" class="form-control">
						<option value="s" selected>양력</option>
						<option value="l">음력</option>
					</select>
				</div>
				<div class="col-lg-1">
					<input class="form-control"	placeholder="2000" name="year" type="number">
				</div>
				<label class="mt-2">년</label>
				<div class="col-lg-1">
					<input class="form-control"	placeholder="12" name="month" type="number">
				</div>
				<label class="mt-2">월</label>
				<div class="col-lg-1">
					<input class="form-control"	placeholder="12" name="date" type="number">
				</div>
				<label class="mt-2">일</label>
			</div>
			<div class="form-group mt-3">
				<label>주 &nbsp; 소</label> <input class="form-control"
					placeholder="16245 경기도 성남시 수정구 수정로 319번지 753동 4701호" name="address" type="text">
			</div>
			<button type="submit" class="btn btn-success">Join</button>
			<button type="reset" class="btn btn-warning">Reset</button>
        </form>
    </th:block>
</th:block>

부트스트랩을 이전 글에서 사용한 것은 잘 동작하지 않으므로, 기존에 다운로드 받아 사용했던 것을 사용하여 작업을 한다.

startbootstrap-simple-sidebar-gh-pages_old.zip
1.05MB

압축을 해제한 후 src/main/resource/static 폴더에 붙여넣기 한다.

assets 폴더의 아이콘은 재사용 하였다.

css 파일은 simple-sidebar.css로 변경하였다. 기존에 사용하던 styles.css를 삭제를 하였다.

js 폴더는 기존 것은 삭제하고 login.js와 join.js만 유지하였다. 다른 것은 vendor 폴더에 있는 것을 사용한다.

기존의 부트스트랩을 적용하면 basic.html도 변경을 해주어야 한다. 기존에 있던 css와 js 부분을 아래와 같이 변경해 준다.

<title>Talanton 스프링 부트 홈 페이지</title>
<!-- Favicon-->
<link rel="icon" type="image/x-icon" th:href="@{/assets/favicon.ico}" />
<!-- Bootstrap core CSS -->
<link th:href="@{/vendor/bootstrap/css/bootstrap.min.css}" rel="stylesheet">
<!-- Custom styles for this template -->
<link th:href="@{/css/simple-sidebar.css}" rel="stylesheet">
<!-- Bootstrap core JS-->
<script th:src="@{/vendor/jquery/jquery.min.js}"></script>
<script th:src="@{/vendor/bootstrap/js/bootstrap.bundle.min.js}"></script>
</head>

전체 basic.html은 다음과 같이 부트스트랩 template가 변경이 되었으므로 역시 수정해 준다.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:fragment="setContent(content)">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <meta name="_csrf" th:content="${_csrf.token}"/>
	<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
    <title>Simple Sidebar - Start Bootstrap Template</title>
    <!-- Bootstrap core CSS -->
    <link th:href="@{/vendor/bootstrap/css/bootstrap.min.css}" rel="stylesheet">
    <!-- Custom styles for this template -->
    <link th:href="@{/css/simple-sidebar.css}" rel="stylesheet">
    <!-- Bootstrap core JavaScript -->
    <script th:src="@{/vendor/jquery/jquery.min.js}"></script>
    <script th:src="@{/vendor/bootstrap/js/bootstrap.bundle.min.js}"></script>
</head>
<body>
<div class="d-flex" id="wrapper">
    <!-- Sidebar -->
    <div class="bg-light border-right" id="sidebar-wrapper">
        <div class="sidebar-heading">Start Bootstrap </div>
        <div class="list-group list-group-flush">
            <a href="#" class="list-group-item list-group-item-action bg-light">Dashboard</a>
            <a href="#" class="list-group-item list-group-item-action bg-light">Shortcuts</a>
            <a href="#" class="list-group-item list-group-item-action bg-light">Overview</a>
            <a href="#" class="list-group-item list-group-item-action bg-light">Events</a>
            <a href="#" class="list-group-item list-group-item-action bg-light">Profile</a>
            <a th:href="@{/sise/list}" class="list-group-item list-group-item-action bg-light">주식 시세 보기</a>
        </div>
    </div>
    <!-- /#sidebar-wrapper -->
    <!-- Page Content -->
    <div id="page-content-wrapper">
        <nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
            <button class="btn btn-primary" id="menu-toggle">Toggle Menu</button>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav ml-auto mt-2 mt-lg-0">
                    <li class="nav-item active">
                        <a class="nav-link" th:href="@{/}">Home <span class="sr-only">(current)</span></a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#">Link</a>
                    </li>
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                            Dropdown
                        </a>
                        <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                            <a sec:authorize="isAuthenticated()" class="dropdown-item logoutBtn" href="#">Logout</a>
                            <a sec:authorize="isAuthenticated()" class="dropdown-item" th:href="@{/member/profile(email=${#authentication.principal.member.email})}">MyPage</a>
                            <a sec:authorize="isAnonymous()" class="dropdown-item" href="/member/login">Login</a>
                            <a sec:authorize="isAnonymous()" class="dropdown-item" href="/member/join">Join</a>
                            <a sec:authorize="isAnonymous()" class="dropdown-item" href="#">아이디 찾기</a>
                            <a sec:authorize="isAnonymous()" class="dropdown-item" href="#">비밀번호 찾기</a>
                        </div>
                    </li>
                </ul>
            </div>
        </nav>
        <div class="container-fluid">
            <th:block th:replace = "${content}"></th:block>
        </div>
    </div>
    <!-- /#page-content-wrapper -->
</div>
<!-- /#wrapper -->
<!-- Menu Toggle Script -->
<script>
	var token = $("meta[name='_csrf']").attr("content");
	var header = $("meta[name='_csrf_header']").attr("content");
	
    $("#menu-toggle").click(function(e) {
        e.preventDefault();
        $("#wrapper").toggleClass("toggled");
    });
    $(".logoutBtn").click(function(e) {
    	e.preventDefault();
    	$.ajax({
    		url: '/member/logout',
    		type: 'post',
    		// CSRF 토큰 값을 헤더로 전송
			beforeSend: function(xhr) {
			    xhr.setRequestHeader(header, token);
			},
			success: function(data) {
				alert("로그아웃 되었습니다.");
				location.href = "/member/login";
			}
    	});
    });
</script>
</body>
</th:block>
</html>

그러면 회원가입 폼은 아래와 같이 표시된다.

- 아이디는 이메일을 사용한다.

- 아이디를 입력하고 중복확인 버튼을 눌러 아이디 중복 확인을 한다.

- 암호와 암호확인을 자바스크립트에서 정규표현식을 사용하여 유효성 검사를 수행한다.

- 이름 : 필수 파라미터 (*로 표시)

- 나머지는 선택 사항으로 입력하지 않아도 회원가입이 수행된다.

자바스크립트에서 회원가입 데이터에 대한 유효성 검사를 수행하고, 아이디 중복 확인 및 회원가입 요청을 서버로 전송한다.

$(document).ready(function() {
	var idCheck = false;	// 아이디 중복 검사 통과 여부
	var passwordValid = false;	// 비밀번호 유효성 검사 결과
	var password2Valid = false;	// 비밀번호 확인 유효성 검사 결과
	var emailValid = false;	// 이메일 유효성 검사 결과
	
	var token = $("meta[name='_csrf']").attr("content");
	var header = $("meta[name='_csrf_header']").attr("content");
	
	var formObj = $(".form-join");
	
	// 이메일 확인 유효성 검사
	$("input[name='email']").on("keyup", function(e) {
		var regExp = /^[-A-Za-z0-9_]+[-A-Za-z0-9_.]*[@]{1}[-A-Za-z0-9_]+[-A-Za-z0-9_.]*[.]{1}[A-Za-z]{2,5}$/;
		if( !regExp.test($("input[name='email']").val()) ) {
			$("input[name='email']").css("color", "#EE5656");
			emailValid = false;
		}
		else {
			$("input[name='email']").css("color", "blue");
			emailValid = true;
		}
	});
	
	// 비밀번호 유효성 검사
	$("input[name='password']").on("keyup", function(e) {
		var regExp = /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{5,15}$/;
		if( !regExp.test($("input[name='password']").val()) ) {
			$("input[name='password']").css("color", "#EE5656");
			passwordValid = false;
		}
		else {
			$("input[name='password']").css("color", "blue");
			passwordValid = true;
		}
	});
	
	// 비밀번호 확인 유효성 검사
	$("input[name='password2']").on("keyup", function(e) {
		var regExp = /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{5,15}$/;
		if( !regExp.test($("input[name='password2']").val()) ) {
			$("input[name='password2']").css("color", "#EE5656");
			password2Valid = false;
		}
		else {
			$("input[name='password2']").css("color", "blue");
			password2Valid = true;
		}
	});

	// 아이디 중복 확인
	$(".btn-secondary").on("click", function(e) {
		var email = $("input[name='email']").val();
		if(email == null || email.length == 0) {
			alert("아이디(이메일 형식)를 입력하세요");
			return;
		}
		if(emailValid == false) {
			alert("고유한 이메일 형식의 아이디를 사용하세요.");
			return;
		}
		// 서버로 중복 확인 요청
		$.ajax({
		  url: '/member/idCheck',
		  data: {email: email},
		  type: 'POST',
		  beforeSend: function(xhr) {
	          xhr.setRequestHeader(header, token);
	      },
		  success: function(result){
			if(result == "ok") {
				idCheck = true;
				alert("사용할 수 있는 아이디 입니다.");
			} else {
				idCheck = false;
				alert("사용할 수 없는 아이디 입니다.");
			}
		  }
		}); //$.ajax
	});
	
	$(".btn-success").on("click", function(e) {
		e.preventDefault();
		var email = $("input[name='email']").val();
		if(email == null || email.length == 0) {
			alert("아이디를 입력하세요");
			email.focus();
			return;
		}
		
		if(idCheck == false) {
			alert("아이디 중복확인을 하세요");
			return;
		}
		
		if(emailValid == false) {
			alert("고유한 이메일 형식의 아이디를 사용하세요.");
			return;
		}
		
		var password = $("input[name='password']").val();
		if(password == null || password.length == 0) {
			alert("암호를 입력하세요");
			password.focus();
			return;
		}
		
		if(passwordValid == false) {
			alert("특수문자, 영문, 숫자의 조합으로 8자 이상 15자 이하를 사용하세요.");
			return;
		}
		
		var password2 = $("input[name='password2']").val();
		if(password2 == null || password2.length == 0) {
			alert("암호 확인을 입력하세요");
			password2.focus();
			return;
		}
		
		if(password2Valid == false) {
			alert("특수문자, 영문, 숫자의 조합으로 8자 이상 15자 이하를 사용하세요.");
			return;
		}
		
		if(password != password2) {
			alert("암호 확인을 입력하세요");
			password2.focus();
			return;
		}
		
		var name = $("input[name='name']").val();
		if(name == null || name.length == 0) {
			alert("이름을 입력하세요");
			name.focus();
			return;
		}

		console.log("submit clicked");
		formObj.submit();
	});
});

이메일 형식의 아이디를 입력하고 중복 확인 버튼을 클릭하면 자바스크립트 join.js에서 아이디를 입력 유무에 대한 유효성 검사를 수행한 후 Ajax를 사용하여 아이디 중복 확인(/member/idCheck)을 서버로 요청한다. 이때 아이디인 email을 파라미터로 전송한다. 또한 CSRF를 header에 전송하여야 한다. 아래 그림을 보면 X-CSRF-TOKEN 헤더에 CSRF 토큰이 전송됨을 확인할 수 있다.

 

MemberController에서는 /member/idCheck 요청에 대하여 처리를 한다.

	@PostMapping("idCheck")
	@ResponseBody
	public String idCheck(String email) {
		log.info("idCheck..." + email);
		if(memberService.idCheck(email)) {
			return "ok";
		}
		else {
			return "nok";
		}
	}

email을 파라미터로 얻고 MemberService의 idCheck() 메소드를 호출하여 아이디 중복 여부를 검사한다. 아이디가 중복되지 않을 경우 true가 반환되며, 브라우저로 "ok" 문자열을 반환한다.

아이디가 중복된 경우 idCheck()에서 false가 반환되며, 브라우저로 "nok" 문자열을 반환한다.

자바스크립트에서는 중복 확인 결과를 받아 사용자에게 알리고, 아이디 중복확인 결과를 변수에 저장한다.

이미 있는 사용자 일 경우는 "사용할 수 없는 아이디 입니다" 경고창이 보여진다.

MemberService.java 인터페이스는 다음과 같다. idCheck()와 join() 메소드를 추가하고 MemberDTO를 엔티티 Member로 변환하는 dtoToEntity() 메소드를 정의한다.

package com.example.sboot.member.service;

import java.util.HashSet;

import com.example.sboot.member.dto.MemberDTO;
import com.example.sboot.member.entity.Member;
import com.example.sboot.member.entity.MemberRole;

public interface MemberService {
	boolean idCheck(String userid);
	void join(MemberDTO member);
	
	default Member dtoToEntity(MemberDTO dto) {
		Member member = Member.builder()
				.email(dto.getEmail())
				.password(dto.getPassword())
				.name(dto.getName())
				.nickname(dto.getNickname())
				.phone(dto.getPhone())
				.address(dto.getAddress())
				.birthday(dto.getCalendar()+"."+dto.getYear()+"."+dto.getMonth()+"."+dto.getDate())
				.fromSocial(false)
				.roleSet(new HashSet<MemberRole>())
				.build();
		member.addMemberRole(MemberRole.USER);
		return member;
	}
}

MemberDTO는 브라우저로부터 입력되는 파라미터 데이터를 수신하기 위하여 정의되며, MemberService에서 Entity로 변환된다. 이때 파라미터의 변환이 수행된다. 어떻게 변환을 할지는 입력 데이터와 데이터베이스에 저장되는 형태에 따라 다를 수 있다.

package com.example.sboot.member.dto;

import java.time.LocalDateTime;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class MemberDTO {
	private String email;
	private String password;
	private String name;
	private String nickname;
	private String phone;
	private String calendar;	// 양력(sonar), 음역(lunar)
	private String year;
	private String month;
	private String date;
	private String address;
	private boolean fromSocial;
	private LocalDateTime regDate;
	private LocalDateTime modDate;
}

MemberServiceImpl.java는 구현 객체로 다음과 같다. 데이터베이스 연동을 위해 MemberRepository를 주입받는다. MemberController로부터 idCheck() 요청을 받으면 MemberRepository로 email을 가진 사용자가 있는지 질의하여 아이디를 가진 사용자가 있는지 알 수 있다.

package com.example.sboot.member.service;

import java.util.Optional;

import javax.transaction.Transactional;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import com.example.sboot.member.dto.MemberDTO;
import com.example.sboot.member.entity.Member;
import com.example.sboot.member.repository.MemberRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Service
@RequiredArgsConstructor
@Log4j2
public class MemberServiceImpl implements MemberService {
	private final MemberRepository memberRepository;
	private final PasswordEncoder pwencoder;

	@Override
	public boolean idCheck(String email) {
		log.info("idcheck..." + email);
		
		Optional<Member> member = memberRepository.findById(email);
		return member.isEmpty();
	}

	@Transactional
	@Override
	public void join(MemberDTO memberDTO) {
		String encPw = pwencoder.encode(memberDTO.getPassword());
		memberDTO.setPassword(encPw);
		Member member = dtoToEntity(memberDTO);
		memberRepository.save(member);
	}
}

회원정보를 입력하고 Join 버튼을 클릭하면, 자바스크립트 join.js에서 회원정보에 대한 유효성 검사를 수행한 후 이상이 없으면 /member/join POST 요청을 서버로 보낸다.

브라우저에서 개발자 도구에서 제공하는 network 탭을 이용하면 서버로 전송되는 데이터를 확인할 수 있다.

MemberController에서는 /member/join POST 요청을 받아 처리한다. MemberService로 회원정보의 저장을 요청하고, 로그인 창으로 리다이렉트를 수행하며, 로그인 절차에서 회원의 아이디를 한번만 활용할 수 있도록 RedirectAttributes로 전달한다.

	@PostMapping("/join")
	public String join(MemberDTO member, RedirectAttributes rttr) {
		log.info("================================");
		log.info("register: " + member);
		memberService.join(member);
		rttr.addFlashAttribute("result", member.getEmail());
		return "redirect:/member/login";
	}

로그인 창으로 이동하며, 회원의 아이디인 이메일이 표시됨을 알 수 있다.

MySQL workbench를 사용하여 회원정보가 member 테이블 및 member_role_set 테이블에 저장이 되었는지를 확인한다.

이로서 회원가입 기능이 잘 동작하는 것을 확인할 수 있다.

password에 대하여 정책을 특수문자, 영문자, 숫자의 조합으로 6자 이상으로 사용하도록 요구하고 있으므로, 이전에 login.html(login.js)에서 유효성 검사를 위해 임시로 막아두었던 코드를 다시 수정하여 유효성 검사가 제대로 수행이 되도록한다.

	// 비밀번호 유효성 검사
	$("input[name='password']").on("keyup", function(e) {
		var regExp = /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{5,15}$/;
		if( !regExp.test($("input[name='password']").val()) ) {
			$("input[name='password']").css("color", "#EE5656");
			passwordValid = false;
		}
		else {
			$("input[name='password']").css("color", "blue");
			passwordValid = true;
		}
		passwordValid = true;
	});

로그인 절차가 잘 수행이 된다. 비밀번호에 대하여 특수문자, 영문자, 숫자의 조합으로 6자 이상을 입력하여야 로그인이 진행된다.

전체 소스코드는 다음과 같다.

sboot.zip
2.22MB

Posted by 세상을 살아가는 사람
,