
새 프로젝트 생성
➡️ 권한 별로 테이블을 따로 만들어도 되고, 같은 테이블 안에서 권한을 추가로 줘도 된다
권한 생성시 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) 비 로그인 인 사용자가 판매자 페이지로 들어간 경우 로그인 창이 뜬다
로그인창에서 로그인 후 판매자 홈으로 바로 이동 가능하게끔 설정
⇒ 추가기능 설정하면 가능하다! 다음에 구현해볼것