새 프로젝트 생성
➡️ 권한 별로 테이블을 따로 만들어도 되고, 같은 테이블 안에서 권한을 추가로 줘도 된다
권한 생성시 null이 아님을 체크한 후 기본값을 주는 경우,
새로운 컬럼이 생성될 떄 기존에 존재하는 데이터는 값이 비어있으므로
null이 아닌 조건을 만족해야하니 기본값이 자동으로 체워진다
서버 구동시 터미널에서 오류, 출력결과확인 등 모든 정보를 확인해야 한다
환경적인 요소에서 문제가 발생했을 때 해당문제가 해결되지 않은 경우 서버가 실행되지 않는다
<!-- oracle -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
Application.java 실행시에 환경설정을 읽어들이는데
환경설정에 오류가 있다면 서버 구동이 안된다!
환경설정에 오류가 없어야 Controller, Mapper 등 다른 파일들이 실행된다
# 서버주소
# 127.0.0.1:8080/BOOT1/
server.port=8080
# 나중에 프로젝트시 여러명 사용시에는 여러개 서버 생성해준다
# server.servlet.context-path=/BOOT2
server.servlet.context-path=/BOOT1
# 소스코드 변경시 자동으로 서버 구동하기
spring.devtools.livereload.enabled=true
# view에 해당하는 html의 위치설정
# cache=false 개발시, 서비스 배포시에는 true
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
# oracle
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@1.234.5.158:11521/xe
spring.datasource.username=ds207
spring.datasource.password=pw207
# mysql, mariadb
# spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
# spring.datasource.url=jdbc:mysql://localhost:3306/DB
# spring.datasource.username=아이디
# spring.datasource.password=암호
spring.datasource.hikari.connection-test-query=SELECT 1 FROM DUAL
spring.datasource.hikari.connection-timeout=600000
spring.datasource.hikari.maximum-pool-size=500
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.minimum-idle=20
spring.datasource.hikari.validation-timeout=3000
spring.datasource.hikari.idle-timeout=60000
memberMapper.xml
사용을 위해memberMapper.xml
가 위치한 mappers 폴더의 위치를 설정해주는config
파일을 생성한다
➡️ config 파일은 사용자가 임의로 만든 폴더이므로 Application.java에 등록해줘야 한다📃 로직
1.Service
에서는memberMapper.java
를 사용하고
memberMapper.java
에서는 쿼리문을 생략하여 작성한다
2. 생략된 쿼리문은memberMapper.xml
에 작성되어 있다
3.memberMapper.xml
에namespace
로 작성된 쿼리문이 위치할 경로"com.example.mapper.MemberMapper"
를 지정해준다
- 클래스명은 개발자 마음대로 생성하되,
@
로 역할을 명시해준다
➡️@Configuration
= 환경설정 파일- 원래 클래스는 메소드에 의해 호출되어야 실행되지만
@Bean
이 붙으면 자동으로 서버 실행시 구동된다
➡️@Bean
= 객체 생성
.getResources("classpath:/mappers/*Mapper.xml");
getResources
= Resources를 가져온다classpath
= Resources를 의미한다/mappers/
= mappers 폴더안의*Mapper.xml
= Mapper.xml 로 끝나는 모든 파일을 찾는다
@Configuration
@Slf4j
public class MybatisConfig {
// @Bean =>객체 생성
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// 서버구동시 이 부분 출력
log.info("datasource configuration");
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
// xml mappers 위치 설정 ex) resources폴더에 /mappers/memberMapper.xml
Resource[] arrResource = new PathMatchingResourcePatternResolver().getResources("classpath:/mappers/*Mapper.xml");
sqlSessionFactoryBean.setMapperLocations(arrResource);
return sqlSessionFactoryBean.getObject();
}
}
- 임의로 만든 config 파일을 application에 등록
- mapper 사용 위한 파일 경로 지정
// 서비스,컨트롤러 환경설정
@ComponentScan(basePackages = {
"com.example.service",
"com.example.controller",
"com.example.config"
})
// mybatis => mapper
// @MapperScan => mapper를 쓰기 위한 위치 지정
@MapperScan(basePackages = "com.example.mapper")
namespace
로mapper.xml
의 위치를 설정해준다
="com.example.mapper.MemberMapper”
- 회원가입을 위한 쿼리문을 작성한다
➡️parameterType
을MemberDTO
로 설정하여 한번에 보낸다
xml파일 작성시 첫번째 줄은
<?xml version
~ 으로 시작해야한다
첫번째 줄이 공백으로 시작할 경우 작동하지 않는다
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 회원가입 -->
<mapper namespace="com.example.mapper.MemberMapper">
<insert id="joinMember" parameterType="com.example.dto.MemberDTO">
INSERT INTO MEMBERTBL(USERID, USERPW, AGE, PHONE, GENDER, REGDATE, ROLE)
VALUES(#{userid}, #{userpw}, #{age}, #{phone}, #{gender}, CURRENT_DATE, #{role})
</insert>
</mapper>
@Mapper
를 지정해준다
- MemberMapper.java 의
인터페이스명MemberMapper
과 메서드명joinMember
은
➡️ memberMapper.xml 의
namespace="com.example.mapper.MemberMapper
,id="joinMember"
와 일치해야 한다- MemberMapper.java의
인터페이스명
과 memberMapper.xml의namespace
로 mapper를 검색하기 때문에namespace
는 고유해야 한다
@Mapper
public interface MemberMapper {
// 여기를 생략했기 때문에 xml에서 찾아서 자동으로 수행한다
public int joinMember( MemberDTO member );
}
DB 입력시 데이터를 개별로 보낼수도 있지만, DTO를 이용하여 한번에 보내는게 효율적이다
➡️ 생성된 DTO는 Controller에서 사용
Oracle
MEMBERTBL
에 설정한 타입을 참고하여 생성
💡join.html
의name
값과MemberDTO
에 지정한변수명
이 일치해야 데이터가 입력된다
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class MemberDTO {
String userid;
String userpw;
int age;
String phone;
String gender;
Date regdate;
String role;
}
Oracle MEMBERTBL을 참고하여 회원가입에 필요한 항목을 생성한다
input type
이 사용자가 직접 입력하는 경우가 아닌radio
나checkbox
같은 경우 DB에 저장할 값을 value를 지정하여 명시해준다- SUBMIT 제줄시 form안의 작업을 수행한다
➡️form th:action="@{/member/joinaction.do}" method="post”
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>회원가입</title>
</head>
<body>
<h3>회원가입</h3>
<form th:action="@{/member/joinaction.do}" method="post">
<hr /><br />
아이디 : <input type="text" name="userid" /><br />
암호 : <input type="password" name="userpw"/><br />
암호확인 : <input type="password" /><br />
나이 : <input type="number" name="age"/><br />
연락처 : <input type="text" name="phone"/><br />
성별 :
<input type="radio" name="gender" value="M"/>남
<input type="radio" name="gender" value="F"/>여
<br />
권한 :
<select name="role">
<option value="CUSTOMER">고객</option>
<option value="SELLER">판매자</option>
<option value="ADMIN">운영자</option>
</select>
<hr /><br />
<input type="submit" value="회원가입">
</form>
</body>
</html>
DB 전송 로직
1번이 정석! 지금은 실습이니 service 생략하고 2번으로 실행
- controller ⇒ service/serviceImpl ⇒ mapper + xml
- controller ⇒ mapper + xml
@PostMapping
작성하여 입력받은 값을 DB에 저장
@ModelAttribute
사용하여 MemberDTO로 데이터 전송DB 전송 로직
@GetMapping
에서 submit
⇒@PostMapping(value = "/joinaction.do")
로 이동
⇒ DB에 데이터 전송 완료 후return "redirect:/home.do";
- 출력시
{}
대괄호를 입력해야 전달값MemberDTO
을 확인해 볼 수 있다final String format = "Member => {} ";
- 회원가입시 pw 암호화 안됨
➡️MemberController.java
에서Security
의BCryptPasswordEncoder
이용하여
암호 입력시salt값 생성
+hashpw
로 저장하도록 한다
*BCryptPasswordEncoder
이용시 Security 내에서 salt값이 시스템 내에서 자동으로 생성된다
➡️ 회원가입시 같은 암호를 넣어도 hash값이 다르게 생성된다
@Controller
@RequestMapping(value = "/member")
@Slf4j
public class MemberController {
@Autowired
MemberMapper mMapper;
// 출력용 포맷 ! 임의로 지정한 이름 Member
final String format = "Member => {} ";
// 회원가입 페이지로 이동
// 127.0.0.1:8080/BOOT1/member/join.do
@GetMapping(value = "/join.do")
public String joinGET(){
// resources/templates/member폴더생성/join파일생성
return "member/join";
}
// 회원가입 DB전송
@PostMapping(value = "/joinaction.do")
public String joinPOST(@ModelAttribute MemberDTO member){
// 잘 오는지 확인
log.info(format, member.toString());
// security 내에서 salt값 생성
BCryptPasswordEncoder bcpe = new BCryptPasswordEncoder();
String hashPW = bcpe.encode(member.getUserpw());
member.setUserpw(hashPW);
// DB처리
// 회원가입 항목 6개를 한번에 받는다
int ret = mMapper.joinMember(member);
if(ret == 1) {
// 수행후 적절한 페이지로 이동
// http://127.0.0.1:8080/BOOT1/home.do
return "redirect:/home.do";
}
return "redirect:/member/join.do";
}
Spring을 사용할 때 애플리케이션에 대한 인증, 권한 부여 등의 보안 기능을 제공하는 프레임워크
➡️ 다양한 로그인 방법(Form, OAuth2, JWT 등...)에 대해 Spring이 일부를 구현했으니 이용시 수정(확장)하여 사용한다
홈 화면
GET => 127.0.0.1:8080/BOOT1/home.do
- 회원가입
GET => 127.0.0.1:8080/BOOT1/member/join.do
POST => 127.0.0.1:8080/BOOT1/member/joinaction.do- 로그인
GET => 127.0.0.1:8080/BOOT1/member/login.do권한별 홈 화면
- 관리자 홈 화면
GET => 127.0.0.1:9090/BOOT1/admin/home.do- 판매자 홈 화면
GET => 127.0.0.1:9090/BOOT1/seller/home.do- 고객 홈 화면
GET => 127.0.0.1:9090/BOOT1/customer/home.do
➡️ Security 이용하여 로그인 후 사용자의 권한에 해당하는 홈페이지만 접근이 가능하다
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
SecurityConfig.java에 환경설정하기
➡️ Security 사용을 위해 생성
SecurityConfig가 Controller 역할까지 한다
로그인 페이지로 이동시 GET
인 경우 화면 직접 생성, 로그인 처리하는 POST
는 Security 사용
로그인 페이지 설정시
로그인 화면 GET
MemberController에 생성, 로그인 처리 POST
는 Security 사용
로그아웃 페이지 설정시
GET
사용 불가 , POST
로 사용
🤷♀️ 왜 로그아웃 시
POST
를 사용해야 할까?
👩🔧 로그아웃은 클라이언트에서 서버로 요청하는 것이니GET
,POST
중에서 선택해야 하는데,
Get
을 사용해도 동작하지 않는것은 아니지만 문제가 발생하는 경우가 있기 때문에POST
사용이 권장된다
💡 Spring Security 공식문서 참고
페이지별 접근권한 설정
로그인, 회원가입 페이지는 모든 권한이 접근 가능
로그인 이후 획득한 권한별로 접근 가능한 곳 제한
hasAnyAuthority
이용하여 관리자는 판매자 홈페이지도 접근 가능하도록 설정
- hasAuthority ➡️ 권한을 하나만 지정
- hasAnyAuthority ➡️ 권한을 많이 지정할 수 있다! 목록 형태가 온다는 걸 알 수 있다
접근 불가 페이지 설정
로그인한 사용자의 권한이 접근하려는 페이지의 권한에 접근할수 없는 경우,
접근불가 페이지로 이동하게 설정한다
HomeController에서 생성
➡️ "/page403.do"
= 127.0.0.1:8080/BOOT1/page403
@GetMapping(value = {"/page403"})
public String errorGet(){
return "page403";
}
접근불가 페이지로 이동시 주소는
Controller
에 명시된 주소로 이동하지만accessDeniedPage("/page403.do")
에 의해page403
의 화면이 나타난다
PasswordEncoder
이용하여 로그인시 비밀번호 hash
로 변경 해준다package com.example.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import com.example.service.CustomDetailsService;
@Configuration
@EnableWebSecurity
public class SecurityConfig{
@Autowired
CustomDetailsService customDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
throws Exception {
// 로그인 페이지 설정
// 127.0.0.1:8080/BOOT1/member/login.do
// <form th:action="@{/member/loginaction.do}" method="post">
// <input type="text" name="userid" />
http.formLogin()
.loginPage("/member/login.do")
.loginProcessingUrl("/member/loginaction.do") //post로 가야될 주소
.usernameParameter("userid")
.passwordParameter("userpw")
.defaultSuccessUrl("/home.do") //성공하면 home으로
.permitAll();
// 로그아웃 => GET불가 , POST 사용
http.logout()
.logoutUrl("/member/logoutaction.do")
.logoutSuccessUrl("/home.do") //로그아웃 성공시
.invalidateHttpSession(true)
.clearAuthentication(true)
.permitAll();
// 페이지별 접근권한 설정
// 관리자권한은 판매자 페이지도 접근 가능하다
http.authorizeRequests()
// 관리자 페이지는 관리자권한만 접근가능하다
.antMatchers("/admin", "/admin/**").hasAuthority("ADMIN") // hasAuthority
// 판매자 페이지는 관리자권한과 판매자권한만 접근 가능하다
.antMatchers("/seller", "/seller/**").hasAnyAuthority("ADMIN", "SELLER")
// 고객 페이지는 고객권한만 접근 가능하다
.antMatchers("/customer", "/customer/**").hasAuthority("CUSTOMER")
.anyRequest().permitAll();
http.userDetailsService(customDetailsService);
// 접근 불가 페이지 설정
http.exceptionHandling().accessDeniedPage("/page403.do");
return http.build();
}
// 로그인시 비밀번호 hash로 변경
// 성공하면 홈으로 실패하면 그자리
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
로그인 생성
@GetMapping(value = "/login.do")
public String loginGET() {
return "member/login";
}
Spring Security에서 유저의 정보를 불러오기 위해서 구현해야하는 인터페이스인
UserDetailsService
를 상속받아@Override
를 작성한다
🌎 참고링크 : Spring Security UserDetails, UserDetailsService 란?
확인해보면 기본 오버라이드 메서드인loadUserByUsername
을 확인할 수 있다
loadUserByUsername
= 유저의 정보를 불러와서UserDetails
로 리턴
- 리턴타입으로
UserDetails
를 갖는다@param username
으로 어떤 user의 data인지 식별가능하다@return
은 null이 아닌 완전히 채워진 user record를 반환한다
💡 오버라이드는 설계를 변경 할 수 없기 때문에 인터페이스에서 제공하는 형태에 맞추어 반환해야한다
= 인터페이스 기준에 맞게 만들어 내야한다
➡️ 리턴 타입에 맞게 권한을 컬렉션 형태로 변환하고 리턴하는 권한자리에 넣어준다
public UserDetails loadUserByUsername(String username)
- String타입의
username
을 받으면UserDetails
를 반환username
은 변수 이름일 뿐, 사용자에게 입력받은 값을 Security가 내부적으로 처리해username
에 넣어준다log.info
로username
확인하면 사용자의 id값인것을 알 수 있다
MemberDTO member = mMapper.selectMemberOne(username);
username
(사용자의 id값)으로 MemberDTO에서 일치하는 회원의 아이디, 암호 정보 가져오기
반환시 UserDetails 타입의 User 메소드를 반환값으로 갖는다
➡️ UserDetailsService.userdetails.User; 또는 공식문서에서 확인할 수 있다
- 권한은 컬렉션 타입으로 변경후에 넘겨야 한다
🤷♀️권한도 string형이면 get으로 가져올 수 있는데 왜 컬렉션형일까?
👩🔧 한 사람이 여러 권한을 가질수 있기 때문이다
예를 들어, 한사람이 관리자 이면서, 사용자이면서, 판매자일수도 있다
또 어떤 사람은 사용자 이면서 판매자 일 수도 있다
위와 같은 경우 권한 여러개의 권한을 처리 하기 위해 컬렉션 형태를 사용한다String[] strRole = { member.getRole() }; Collection<GrantedAuthority> role = AuthorityUtils.createAuthorityList(strRole);
return new User(아이디, 암호, 권한(=컬렉션타입));
➡️ 반환되면 로그인처리 완료
리턴시 가져올 결과값을 mapper에서 짜준다 = userid가 오면 MemberDTO 반환
사용자 입력란이 빈칸인 경우는 백엔드에서 처리할 필요 없다
➡️ 프론트에서 유효성 검사하고 가져와야함
사용자가 입력한 데이터와 일치하는 회원정보가 없어서 로그인 처리가 안되는 경우 = null
➡️ 터미널 창에서 에러가 난것처럼 보인다
💡 if문 추가하여 회원정보가 null인 경우 와 null이 아닌경우 나누어 처리한다
= 터미널에 에러같이 출력되는것 방지하기위해 작성한 조건문
@Service
@Slf4j
// 시큐리티의 UserDetailsService를 상속받아 @Override를 작성
public class CustomDetailsService implements UserDetailsService{
final String format = "SECURITY => {} ";
@Autowired
MemberMapper mMapper;
@Override
// String타입의 username을 받으면 UserDetails를 반환한다
// 아이디(username)가 전송되면 UserDetails 타입으로 변환하여 리턴
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// log.info로 username확인하면 사용자의 id인것을 알 수 있다
log.info(format, username);
// 전송된 아이디(username)으로 MemberDTO에서 아이디, 암호 가져오기
MemberDTO member = mMapper.selectMemberOne(username);
if(member != null){ // 입력된 member가 null이 아닌경우 = 회원정보가 있는경우
// 권한 여러개 처리 위해 컬렉션형태 사용 = 권한을 컬렉션으로 변환해야한다
String[] strRole = { member.getRole() };
Collection<GrantedAuthority> role
= AuthorityUtils.createAuthorityList(strRole);
// 최종적인 리턴 타입은 User(아이디, 암호, 권한(=컬렉션타입))형태
// return new User(아이디, 암호, 권한(=컬렉션타입)); => 반환되면 로그인처리 완료
return new User(member.getUserid(), member.getUserpw(), role);
}
else { // 입력된 member 가 null인경우 = 회원정보가 없는경우
String[] strRole = { "_" };
Collection<GrantedAuthority> role = AuthorityUtils.createAuthorityList(strRole);
// User(아이디, 암호, 권한(=컬렉션타입)
return new User(username, "", role);
}
// 빈칸인 경우는 프론트에서 유효성 검사, 백엔드에서 처리할 필요 없음
}
}
+ 추가
객체 가져오기
@Autowired CustomDetailsService
가져온 객체 사용하기
http.userDetailsService(customDetailsService);
mapper.java보고 mapper.xml만들 수 있어야 한다
홈 컨트롤러에서 로그인 여부를 확인하는 세션상태 출력
리턴값 User = UserDetails에서 반환된 User를 이용하여 로그인 성공/실패 출력
출력 결과는 터미널에서 확인이 가능하다
@Controller
public class HomeController {
// 크롬에서 127.0.0.1:8080/BOOT1
// 크롬에서 127.0.0.1:8080/BOOT1/
// 크롬에서 127.0.0.1:8080/BOOT1/home
// 크롬에서 127.0.0.1:8080/BOOT1/home.do
@GetMapping(value = {"/", "/home", "/home.do"})
public String homeGET(@AuthenticationPrincipal User user){ // UserDetails에서 반환된 User
if( user == null ){
System.out.println("로그인 실패");
} else {
System.out.println("로그인 성공");
}
return "home";
}
@GetMapping(value = {"/page403.do"})
public String page403Get(){
return "page403";
}
}
관리자 홈 http://127.0.0.1:8080/BOOT1/admin/home.do
판매자 홈 http://127.0.0.1:8080/BOOT1/seller/home.do
고객 홈 http://127.0.0.1:8080/BOOT1/customer/home.do
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>홈화면</title>
</head>
<body>
<h3>홈화면</h3>
<a th:href="@{/member/login.do}">로그인</a>
<a th:href="@{/member/join.do}">회원가입</a>
<form th:action="@{/member/logoutaction.do}" method="post">
<input type="submit" value="로그아웃">
</form>
<hr />
<a th:href="@{/admin/home.do}">관리자 홈</a>
<a th:href="@{/seller/home.do}">판매자 홈</a>
<a th:href="@{/customer/home.do}">고객 홈</a>
</body>
</html>
접근불가 페이지 생성 ➡️ home과 같은 파일에 생성해야한다
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>접근불가페이지</title>
</head>
<body>
접근불가 페이지 입니다
<a th:href="@{/home.do}"><button>홈으로</button></a>
</body>
</html>
@Controller
@RequestMapping(value="/admin")
public class AdminController {
@GetMapping(value = "/home.do")
public String homeGET(){
return "admin/home";
}
}
@Controller
@RequestMapping(value="/customer")
public class CustomerController {
@GetMapping(value = "/home.do")
public String homeGET(){
return "customer/home";
}
}
@Controller
@RequestMapping(value="/seller")
public class SellerController {
@GetMapping(value = "/home.do")
public String homeGET(){
return "seller/home";
}
}
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>관리자 홈화면</title>
</head>
<body>
관리자 홈화면
</body>
</html>
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>고객 홈화면</title>
</head>
<body>
고객 홈화면
</body>
</html>
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>판매자 홈화면</title>
</head>
<body>
판매자 홈화면
</body>
</html>
👨🏫 로그인, 로그아웃, 권한별 설정대로 접근이 가능한지만 확인/연습
추가적인 기능들은 추후에 배움
ex) 비 로그인 인 사용자가 판매자 페이지로 들어간 경우 로그인 창이 뜬다
로그인창에서 로그인 후 판매자 홈으로 바로 이동 가능하게끔 설정
⇒ 추가기능 설정하면 가능하다! 다음에 구현해볼것