inkoreakim 2 年 前
親
コミット
0d8f13a2d0
28 ファイル変更、444 行追加、64 行削除
  1. 2 0
      build.gradle
  2. 2 2
      src/main/generated/com/example/ili_web/member/domain/QMember.java
  3. 4 2
      src/main/java/com/example/ili_web/auth/CustomAccessDeniedHandler.java
  4. 2 2
      src/main/java/com/example/ili_web/auth/CustomAuthenticationEntryPoint.java
  5. 19 1
      src/main/java/com/example/ili_web/auth/CustomExceptionHandler.java
  6. 15 1
      src/main/java/com/example/ili_web/auth/ErrorController.java
  7. 1 1
      src/main/java/com/example/ili_web/board/repository/BoardRepositoryDsl.java
  8. 1 1
      src/main/java/com/example/ili_web/board/service/BoardService.java
  9. 3 0
      src/main/java/com/example/ili_web/config/SecurityConfig.java
  10. 27 9
      src/main/java/com/example/ili_web/member/controller/MemberController.java
  11. 17 5
      src/main/java/com/example/ili_web/member/domain/Member.java
  12. 1 2
      src/main/java/com/example/ili_web/member/dto/JoinReq.java
  13. 14 0
      src/main/java/com/example/ili_web/member/dto/MemberControlReq.java
  14. 14 1
      src/main/java/com/example/ili_web/member/dto/MemberList.java
  15. 11 1
      src/main/java/com/example/ili_web/member/dto/MemberListReq.java
  16. 16 1
      src/main/java/com/example/ili_web/member/dto/MemberRes.java
  17. 1 0
      src/main/java/com/example/ili_web/member/repository/MemberRepository.java
  18. 48 1
      src/main/java/com/example/ili_web/member/repository/MemberRepositoryDsl.java
  19. 51 4
      src/main/java/com/example/ili_web/member/service/MemberService.java
  20. 14 4
      src/main/resources/static/js/admin/adminChangeInner.js
  21. 8 0
      src/main/resources/static/js/admin/adminGetData.js
  22. 18 1
      src/main/resources/static/js/admin/adminMakeHtml.js
  23. 22 0
      src/main/resources/static/js/admin/adminPublic.js
  24. 56 0
      src/main/resources/static/js/admin/join.js
  25. 23 10
      src/main/resources/templates/html/admin/admin-member.html
  26. 6 0
      src/main/resources/templates/html/admin/backoffice.html
  27. 12 12
      src/main/resources/templates/html/admin/join.html
  28. 36 3
      src/main/resources/templates/html/error/403.html

+ 2 - 0
build.gradle

@@ -53,6 +53,8 @@ dependencies {
     annotationProcessor "jakarta.persistence:jakarta.persistence-api"
     // WebClient
     implementation 'org.springframework.boot:spring-boot-starter-webflux'
+    // 밸리데이션
+    implementation 'org.springframework.boot:spring-boot-starter-validation'
 
     /** Querydsl 설정부 **/
     def generated = 'src/main/generated'

+ 2 - 2
src/main/generated/com/example/ili_web/member/domain/QMember.java

@@ -29,11 +29,11 @@ public class QMember extends EntityPathBase<Member> {
 
     public final StringPath password = createString("password");
 
-    public final StringPath regDt = createString("regDt");
+    public final DateTimePath<java.time.LocalDateTime> regDt = createDateTime("regDt", java.time.LocalDateTime.class);
 
     public final StringPath role = createString("role");
 
-    public final StringPath upDt = createString("upDt");
+    public final DateTimePath<java.time.LocalDateTime> upDt = createDateTime("upDt", java.time.LocalDateTime.class);
 
     public final StringPath userId = createString("userId");
 

+ 4 - 2
src/main/java/com/example/ili_web/auth/CustomAccessDeniedHandler.java

@@ -1,4 +1,5 @@
-package com.example.ili_web.event_handler;
+/*
+package com.example.ili_web.auth;
 
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
@@ -13,6 +14,7 @@ public class CustomAccessDeniedHandler implements AccessDeniedHandler {
     @Override
     public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
         log.error("인가된 사용자가 아닙니다.");
-
+        request.getRequestDispatcher("/error/403").forward(request,response);
     }
 }
+*/

+ 2 - 2
src/main/java/com/example/ili_web/auth/CustomAuthenticationEntryPoint.java

@@ -1,4 +1,4 @@
-package com.example.ili_web.event_handler;
+package com.example.ili_web.auth;
 
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
@@ -11,6 +11,6 @@ import java.io.IOException;
 public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
     @Override
     public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
-        
+
     }
 }

+ 19 - 1
src/main/java/com/example/ili_web/auth/CustomExceptionHandler.java

@@ -1,2 +1,20 @@
-package com.example.ili_web.auth;public class CustomExceptionHandler {
+package com.example.ili_web.auth;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+@ControllerAdvice
+public class CustomExceptionHandler {
+
+    @ExceptionHandler(Exception.class)
+    @ResponseBody
+    public ResponseEntity<?> handleException(HttpServletRequest request, Exception ex) {
+
+        // Exception 예외 발생시 공통 처리 로직 작성
+        return new ResponseEntity<>(ex, HttpStatus.BAD_REQUEST);
+    }
 }

+ 15 - 1
src/main/java/com/example/ili_web/auth/ErrorController.java

@@ -1,2 +1,16 @@
-package com.example.ili_web.auth;public class ErrorController {
+package com.example.ili_web.auth;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+
+@Controller
+@Slf4j
+public class ErrorController {
+
+    @GetMapping("/error/403")
+    public String AuthorizationError() {
+        return "html/error/403.html";
+    }
 }

+ 1 - 1
src/main/java/com/example/ili_web/board/repository/BoardRepositoryDsl.java

@@ -22,7 +22,7 @@ import java.util.List;
 @Repository
 public class BoardRepositoryDsl {
     private final JPAQueryFactory queryFactory;
-    QBoard b = QBoard.board;
+    private final QBoard b = QBoard.board;
 
     public BoardRes boardList(BoardReq req) {
         BoardRes res = new BoardRes();

+ 1 - 1
src/main/java/com/example/ili_web/board/service/BoardService.java

@@ -49,7 +49,7 @@ public class BoardService {
 
     public BoardRes getBoarList(BoardReq req) {
         int currentPage = req.getPage();
-        int pageSize = 10;
+        int pageSize = 5;
         req.setOffset((currentPage - 1) * pageSize);
         req.setLimit(pageSize);
         return boardDsl.boardList(req);

+ 3 - 0
src/main/java/com/example/ili_web/config/SecurityConfig.java

@@ -29,6 +29,9 @@ public class SecurityConfig {
                             .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                             .anyRequest().permitAll();
                 })
+                .exceptionHandling((CustomExceptionHandler
+                        -> CustomExceptionHandler
+                        .accessDeniedPage("/error/403")))
                 .formLogin(form -> form
                         .loginPage("/admin")
                         .loginProcessingUrl("/login")

+ 27 - 9
src/main/java/com/example/ili_web/member/controller/MemberController.java

@@ -1,27 +1,45 @@
 package com.example.ili_web.member.controller;
 
-import com.example.ili_web.member.dto.JoinReq;
-import com.example.ili_web.member.dto.LoginReq;
+import com.example.ili_web.board.dto.SuccessRes;
+import com.example.ili_web.member.dto.*;
 import com.example.ili_web.member.service.MemberService;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.*;
 
 @Controller
 @Slf4j
 @AllArgsConstructor
+@RequestMapping("/api/v1")
 public class MemberController {
 
     private final MemberService memberService;
 
-
-    /*@PostMapping("/join")
-    public ResponseEntity join (@RequestBody JoinReq req) {
+    @PostMapping("/join")
+    public ResponseEntity joinUs(JoinReq req) {
         return memberService.join(req);
-    }*/
+    }
+
+    @PostMapping("/member/check-id")
+    public ResponseEntity checkId (@RequestBody JoinReq req) {
+        return memberService.checkId(req);
+    }
+
+    @GetMapping("/member/list")
+    public ResponseEntity<MemberList> memberList (MemberListReq req) {
+        return memberService.getMemberList(req);
+    }
+
+    @PutMapping("/member/role")
+    public ResponseEntity<SuccessRes> memberRole(@RequestBody MemberControlReq req) {
+        return memberService.memberRole(req);
+    }
+
+    @DeleteMapping("/member")
+    public ResponseEntity<SuccessRes> deleteMember(MemberControlReq req) {
+        return memberService.deleteMember(req);
+    }
 }

+ 17 - 5
src/main/java/com/example/ili_web/member/domain/Member.java

@@ -1,14 +1,13 @@
 package com.example.ili_web.member.domain;
 
 import jakarta.persistence.*;
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
+import lombok.*;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 
 import java.io.Serial;
+import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
 
@@ -16,6 +15,8 @@ import java.util.List;
 @Entity
 @NoArgsConstructor( access = AccessLevel.PROTECTED)
 @Table(name="MEMBER")
+@Builder
+@AllArgsConstructor
 public class Member implements UserDetails {
     @Serial
     private static final long serialVersionUID = 1L;
@@ -39,9 +40,10 @@ public class Member implements UserDetails {
     @Column
     private String useYn;
     @Column
-    private String regDt;
+    private LocalDateTime regDt;
     @Column
-    private String upDt;
+    private LocalDateTime upDt;
+
 
     @Override
     public Collection<? extends GrantedAuthority> getAuthorities() {
@@ -76,4 +78,14 @@ public class Member implements UserDetails {
     public boolean isEnabled() {
         return true;
     }
+
+    public void updateMemberRole(String role) {
+        this.role = role;
+        this.upDt = LocalDateTime.now();
+    }
+
+    public void deleteMember(String n) {
+        this.useYn = n;
+        this.upDt = LocalDateTime.now();
+    }
 }

+ 1 - 2
src/main/java/com/example/ili_web/member/dto/JoinReq.java

@@ -4,11 +4,10 @@ import lombok.Data;
 
 @Data
 public class JoinReq {
-    private String id;
+    private String userId;
     private String password;
     private String name;
     private String email;
-    private String address;
     private String role;
     private String useYn;
     private String regDt;

+ 14 - 0
src/main/java/com/example/ili_web/member/dto/MemberControlReq.java

@@ -0,0 +1,14 @@
+package com.example.ili_web.member.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.apache.logging.log4j.core.config.plugins.validation.constraints.NotBlank;
+
+@NoArgsConstructor
+@Getter
+@Setter
+public class MemberControlReq {
+    @NotBlank
+    private Long id;
+}

+ 14 - 1
src/main/java/com/example/ili_web/member/dto/MemberList.java

@@ -1,2 +1,15 @@
-package com.example.ili_web.member.dto;public class MemberList {
+package com.example.ili_web.member.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import java.util.List;
+
+@Getter
+@Builder
+public class MemberList {
+    private List<MemberRes> memberList;
+    private int offset;
+    private int page;
+    private Long total;
 }

+ 11 - 1
src/main/java/com/example/ili_web/member/dto/MemberListReq.java

@@ -1,2 +1,12 @@
-package com.example.ili_web.member.dto;public class MemberListReq {
+package com.example.ili_web.member.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class MemberListReq {
+    private int page;
+    private int size;
+    private int offset;
 }

+ 16 - 1
src/main/java/com/example/ili_web/member/dto/MemberRes.java

@@ -1,2 +1,17 @@
-package com.example.ili_web.member.dto;public class MemberRes {
+package com.example.ili_web.member.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@Getter
+public class MemberRes {
+    private Long id;
+    private String userId;
+    private String name;
+    private String email;
+    private String role;
+    private String useYn;
+    private LocalDateTime regDt;
 }

+ 1 - 0
src/main/java/com/example/ili_web/member/repository/MemberRepository.java

@@ -5,4 +5,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
 
 public interface MemberRepository extends JpaRepository<Member, Long> {
     Member findByUserId(String userId);
+    boolean existsByUserId(String userId);
 }

+ 48 - 1
src/main/java/com/example/ili_web/member/repository/MemberRepositoryDsl.java

@@ -1,2 +1,49 @@
-package com.example.ili_web.member.repository;public class MemberRepositoryDsl {
+package com.example.ili_web.member.repository;
+
+import com.example.ili_web.member.domain.QMember;
+import com.example.ili_web.member.dto.MemberList;
+import com.example.ili_web.member.dto.MemberListReq;
+import com.example.ili_web.member.dto.MemberRes;
+import com.querydsl.core.types.Projections;
+import com.querydsl.core.types.dsl.DateTemplate;
+import com.querydsl.core.types.dsl.Expressions;
+import com.querydsl.core.types.dsl.StringTemplate;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@RequiredArgsConstructor
+@Repository
+public class MemberRepositoryDsl {
+    private final JPAQueryFactory queryFactory;
+    private QMember m = QMember.member;
+
+    public MemberList getMemberList(MemberListReq req) {
+        List<MemberRes> list = queryFactory
+                .select(Projections.fields(MemberRes.class, m.id, m.userId, m.name, m.email, m.role, m.useYn, m.regDt))
+                .from(m)
+                .where(m.useYn.eq("Y"))
+                .offset(req.getOffset())
+                .limit(req.getSize())
+                .orderBy(m.id.desc())
+                .fetch();
+
+        Long total = queryFactory
+                .select(m.count())
+                .from(m)
+                .where(m.useYn.eq("Y"))
+                .orderBy(m.id.desc())
+                .fetchOne();
+
+        total = total != null && total > 10 ? total / 10 : 1;
+        return MemberList.builder()
+                .memberList(list)
+                .offset(req.getOffset())
+                .page(req.getPage())
+                .total(total)
+                .build();
+    }
 }

+ 51 - 4
src/main/java/com/example/ili_web/member/service/MemberService.java

@@ -1,14 +1,20 @@
 package com.example.ili_web.member.service;
 
+import com.example.ili_web.board.dto.SuccessRes;
 import com.example.ili_web.member.domain.Member;
-import com.example.ili_web.member.dto.JoinReq;
-import com.example.ili_web.member.dto.LoginReq;
+import com.example.ili_web.member.dto.*;
 import com.example.ili_web.member.repository.MemberRepository;
+import com.example.ili_web.member.repository.MemberRepositoryDsl;
+import jakarta.persistence.EntityNotFoundException;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
 
 @Service
 @Slf4j
@@ -16,6 +22,7 @@ import org.springframework.stereotype.Service;
 public class MemberService {
 
     private final MemberRepository repository;
+    private final MemberRepositoryDsl repositoryDsl;
     private final PasswordEncoder encoder;
 
     public ResponseEntity<?> login (LoginReq req) {
@@ -31,7 +38,47 @@ public class MemberService {
         }
     }
 
-    public ResponseEntity join(JoinReq req) {
-        return ResponseEntity.ok(null);
+    public ResponseEntity<SuccessRes> join(JoinReq req) {
+        Member member = Member.builder()
+                .userId(req.getUserId())
+                .password(encoder.encode(req.getPassword()))
+                .name(req.getName())
+                .email(req.getEmail())
+                .role("WAIT")
+                .useYn("Y")
+                .regDt(LocalDateTime.now())
+                .upDt(LocalDateTime.now())
+                .build();
+        repository.save(member);
+        return ResponseEntity.ok(new SuccessRes("200", "회원가입성공"));
+    }
+
+    public ResponseEntity checkId(JoinReq req) {
+        if (repository.existsByUserId(req.getUserId())) {
+            return ResponseEntity.ok().body("중복된 아이디입니다.");
+        } else {
+            return ResponseEntity.ok().body("사용 가능한 아이디입니다.");
+        }
+    }
+
+    public ResponseEntity<MemberList> getMemberList(MemberListReq req) {
+        req.setOffset((req.getPage() -1) * req.getSize());
+        return ResponseEntity.ok(repositoryDsl.getMemberList(req));
+    }
+
+    @Transactional
+    public ResponseEntity<SuccessRes> memberRole(MemberControlReq req) {
+        Member member = repository.findById(req.getId()).orElseThrow(()
+                ->new EntityNotFoundException("Member Not Found"));
+        member.updateMemberRole("ADMIN");
+        return ResponseEntity.ok(new SuccessRes("200", "수정 완료"));
+    }
+
+    @Transactional
+    public ResponseEntity<SuccessRes> deleteMember(MemberControlReq req) {
+        Member member = repository.findById(req.getId()).orElseThrow(()
+                ->new EntityNotFoundException("Member Not Found"));
+        member.deleteMember("N");
+        return ResponseEntity.ok(new SuccessRes("200", "삭제 완료"));
     }
 }

+ 14 - 4
src/main/resources/static/js/admin/adminChangeInner.js

@@ -10,13 +10,20 @@ async function adminChangeInner(name) {
         target = 'advice_list_table';
     }
     // html 가져오기
+
     await axios.get(`/html/admin/${name}.html`)
         .then(async (res) => {
             element.innerHTML = res.data;
-            const list = await getList(code, 1);
-            target = document.getElementById(target);
-            adminMakeTable(list.data.dataList, target, code);
-            adminMakePagenation(list.data, code);
+            if (!name.includes('member')) {
+                const list = await getList(code, 1);
+                target = document.getElementById(target);
+                adminMakeTable(list.data.dataList, target, code);
+                adminMakePagination(list.data, code);
+            } else {
+                const data = await getMemberList(1);
+                adminMemberMakeTable(data);
+                console.log(data);
+            }
         })
 }
 
@@ -44,3 +51,6 @@ function goToInsert() {
         })
 }
 
+function goToMemberDetail() {
+
+}

+ 8 - 0
src/main/resources/static/js/admin/adminGetData.js

@@ -29,4 +29,12 @@ function bindDetail (data) {
 
 function makeInfo (userName, regDt) {
     return `${userName} - ${regDt}`;
+}
+
+function getMemberList(page) {
+    try {
+        return axios.get('/api/v1/member/list', {params : {page : page, size : 5}});
+    } catch (e) {
+
+    }
 }

+ 18 - 1
src/main/resources/static/js/admin/adminMakeHtml.js

@@ -11,7 +11,24 @@ function adminMakeTable(list, target, code) {
     target.innerHTML = html;
 }
 
-function adminMakePagenation(data, type) {
+function adminMemberMakeTable (list) {
+    let html = '';
+    let dataList = list.data.memberList;
+    for (let data of dataList) {
+        html += '<tr>';
+        html += `<td><a href="javascript:void(0);" onclick="goToMemberDetail('${data.id}')">${data.userId}</a></td>`;
+        html += `<td>${data.name}</td>`;
+        html += `<td>${data.email}</td>`;
+        html += `<td>${formatDate(data.regDt)}</td>`;
+        html += data.role === 'WAIT'?
+            `<td><button type="button" class="btn btn-success" onclick="memberAccess('${data.id}')">승인</button></td>`
+            : `<td><button type="button" class="btn btn-danger" onclick="deleteMember('${data.id}')">삭제</button></td>`
+        html += '</tr>';
+    }
+    document.getElementById('member_list_table').innerHTML = html;
+}
+
+function adminMakePagination(data, type) {
     let paginationHtml = '<ul class="pagination justify-content-center">';
 
     // Previous 버튼

+ 22 - 0
src/main/resources/static/js/admin/adminPublic.js

@@ -91,6 +91,28 @@ function saveComment() {
 
 }
 
+function memberAccess (id) {
+    axios.put('/api/v1/member/role', {id})
+        .then(res=> {
+            alert('승인이 완료되었습니다.');
+            adminChangeInner('admin-member');
+        })
+        .catch(error => {
+
+        })
+}
+
+function deleteMember (id) {
+    axios.delete('/api/v1/member', {params:{id}})
+        .then(res=> {
+            alert('삭제가 완료되었습니다.');
+            adminChangeInner('admin-member');
+        })
+        .catch(error => {
+
+        })
+}
+
 function nullCheck (data) {
     return !data || !data.trim();
 }

+ 56 - 0
src/main/resources/static/js/admin/join.js

@@ -0,0 +1,56 @@
+function signUp () {
+    const joinData = document.getElementById('join_form');
+    let formData = new FormData(joinData);
+    let result = validationJoin(formData);
+    if (result === 'SUCCESS') {
+        axios.post('/api/v1/join', formData)
+            .then(res=>{
+                alert('회원가입이 완료되었습니다.');
+                location.href = '/admin';
+            })
+            .catch(error=>{
+                alert('회원가입 실패');
+            })
+    } else {
+        alert(result);
+    }
+}
+
+
+function validationJoin (formData) {
+    if (formData.get('userId').length > 20 || formData.get('userId').length < 2) {
+        return '아이디가 올바르지 않습니다.';
+    } else if (formData.get('password').length > 20 || formData.get('password').length < 4) {
+        return '비밀번호가 올바르지 않습니다';
+    } else if (formData.get('name').length > 10 || formData.get('userId').length < 2) {
+        return '이름을 확인해주세요';
+    } else if (!formData.get('email').includes('@') || !formData.get('email').includes('.')) {
+        return '이메일 형식을 확인해주세요';
+    } else {
+        return 'SUCCESS';
+    }
+}
+
+const duplicationCheck = (userId) => {
+    return axios.post('/api/v1/member/check-id', {userId})
+}
+
+document.getElementById('member_id').addEventListener('blur', async ()=> {
+    const id = document.getElementById('member_id').value;
+    const span = document.getElementById('dupl_text')
+    if (id.length < 20 || id.length > 2) {
+        const check = await duplicationCheck(id);
+        span.innerText = check.data;
+        if (check.data.includes('중복')) {
+            span.classList.remove('text-danger');
+            span.classList.remove('text-primary');
+            span.classList.add('text-danger');
+            document.getElementById('member_id').value = '';
+        } else {
+            span.classList.remove('text-danger');
+            span.classList.remove('text-primary');
+            span.classList.add('text-primary');
+        }
+        span.style.display = 'block';
+    }
+})

+ 23 - 10
src/main/resources/templates/html/admin/admin-member.html

@@ -1,10 +1,23 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-  <meta charset="UTF-8">
-  <title>$Title$</title>
-</head>
-<body>
-$END$
-</body>
-</html>
+<div style="display: flex; justify-content: space-between; align-items: center;">
+    <h2>회원 관리</h2>
+    <!--<button type="button" class="btn btn-success" onclick="goToInsert()">게시글 작성</button>-->
+</div>
+<div class="table-responsive" id="admin_member_table">
+    <table class="table table-striped table-sm">
+        <thead>
+        <tr>
+            <th scope="col">계정</th>
+            <th scope="col">이름</th>
+            <th scope="col">이메일</th>
+            <th scope="col">등록일</th>
+            <th scope="col">관리</th>
+        </tr>
+        </thead>
+        <tbody id="member_list_table">
+
+        </tbody>
+    </table>
+    <nav aria-label="Page navigation" id="page_tag">
+
+    </nav>
+</div>

+ 6 - 0
src/main/resources/templates/html/admin/backoffice.html

@@ -43,6 +43,12 @@
                             홈
                         </a>
                     </li>
+                    <li class="nav-item">
+                        <a class="nav-link" href="javascript:void(0);" th:onclick="adminChangeInner('admin-member')">
+                            <span data-feather="file"></span>
+                            계정 관리
+                        </a>
+                    </li>
                     <li class="nav-item">
                         <a class="nav-link" href="javascript:void(0);" th:onclick="adminChangeInner('admin-notice')">
                             <span data-feather="file"></span>

+ 12 - 12
src/main/resources/templates/html/admin/join.html

@@ -21,37 +21,37 @@
 <body class="text-center">
 
 <main class="form-signin">
-    <form action="/login" method="post" enctype="multipart/form-data" id="join_form">
+    <form id="join_form">
         <img class="mb-4" src="/image/logo.png" alt="" width="200" height="57">
         <h1 class="h3 mb-3 fw-normal">회원가입</h1>
 
         <div class="form-floating">
-            <input type="email" class="form-control" id="member_id" placeholder="name@example.com" name="userId">
-            <label for="member_id">아이디</label>
+            <input type="text" class="form-control" id="member_id" placeholder="name@example.com" name="userId">
+            <label for="member_id">아이디 (2~20자)</label>
         </div>
         <div class="form-floating">
-            <input type="password" class="form-control" id="member_password" placeholder="Password" name="password">
-            <label for="member_password">비밀번호</label>
+            <p class="text-danger" id="dupl_text" style="display: none;"></p>
         </div>
         <div class="form-floating">
-            <input type="password" class="form-control" id="member_name" placeholder="Password" name="password">
-            <label for="member_password">비밀번호</label>
+            <input type="password" class="form-control" id="member_password" placeholder="Password" name="password">
+            <label for="member_password">비밀번호 (4~20자)</label>
         </div>
         <div class="form-floating">
-            <input type="password" class="form-control" id="member_email" placeholder="Password" name="password">
-            <label for="member_password">비밀번호</label>
+            <input type="text" class="form-control" id="member_name" placeholder="Name" name="name">
+            <label for="member_password">이름 (2~10자)</label>
         </div>
         <div class="form-floating">
-            <input type="password" class="form-control" id="member_address" placeholder="Password" name="password">
-            <label for="member_password">비밀번호</label>
+            <input type="email" class="form-control" id="member_email" placeholder="Email" name="email">
+            <label for="member_password">email</label>
         </div>
 
         <div class="form-floating" style="display: flex;justify-content: space-between;margin-top: 20px">
-            <button class="w-100 btn btn-lg btn-success" type="button" onclick="goToJoin()">가입신청</button>
+            <button class="w-100 btn btn-lg btn-success" type="button" onclick="signUp()">가입신청</button>
         </div>
     </form>
 </main>
 </body>
 <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
 <script src="/js/admin/login.js"></script>
+<script src="/js/admin/join.js"></script>
 </html>

+ 36 - 3
src/main/resources/templates/html/error/403.html

@@ -1,10 +1,43 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-  <meta charset="UTF-8">
-  <title>$Title$</title>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>403 Forbidden</title>
+    <!-- Bootstrap CSS -->
+    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
+    <style>
+        .error-page {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            height: 100vh;
+            text-align: center;
+        }
+
+        .error-page h1 {
+            font-size: 100px;
+            font-weight: bold;
+            color: #343a40;
+        }
+
+        .error-page p {
+            font-size: 20px;
+            color: #6c757d;
+        }
+    </style>
 </head>
 <body>
-$END$
+<div class="container error-page">
+    <div>
+        <h1>403</h1>
+        <p>Forbidden - 승인대기 중입니다.</p>
+    </div>
+</div>
+
+<!-- Optional JavaScript and Bootstrap JS -->
+<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.7.0/dist/umd/popper.min.js"></script>
+<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
 </body>
 </html>