From 601cd6faf3f4b5f5412857f615bc90593c121c9c Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Wed, 21 Feb 2024 23:11:40 +0900 Subject: [PATCH 01/25] =?UTF-8?q?=ED=8A=B8=EB=A6=AC=EC=83=9D=EC=84=B1=20in?= =?UTF-8?q?terface=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +- .../tree/controller/TreeController.java | 10 +++++ .../chukapoka/server/tree/entity/Tree.java | 43 +++++++++++++++++++ .../tree/repository/TreeRepository.java | 12 ++++++ .../server/tree/service/TreeService.java | 17 ++++++++ .../server/tree/service/TreeServiceImpl.java | 33 ++++++++++++++ .../server/user/dto/UserResponseDto.java | 5 --- .../user/repository/UserRepository.java | 3 +- 8 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/chukapoka/server/tree/controller/TreeController.java create mode 100644 src/main/java/com/chukapoka/server/tree/entity/Tree.java create mode 100644 src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java create mode 100644 src/main/java/com/chukapoka/server/tree/service/TreeService.java create mode 100644 src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java diff --git a/build.gradle b/build.gradle index 43232a4..485b8ef 100644 --- a/build.gradle +++ b/build.gradle @@ -35,9 +35,9 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-validation") // PostgreSQL JDBC 드라이버 의존성 -// runtimeOnly 'org.postgresql:postgresql' + runtimeOnly 'org.postgresql:postgresql' // h2 - runtimeOnly 'com.h2database:h2' +// runtimeOnly 'com.h2database:h2' // Jakarta Validation API 의존성 diff --git a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java new file mode 100644 index 0000000..6de5ca4 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java @@ -0,0 +1,10 @@ +package com.chukapoka.server.tree.controller; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/tree") +public class TreeController { +} + diff --git a/src/main/java/com/chukapoka/server/tree/entity/Tree.java b/src/main/java/com/chukapoka/server/tree/entity/Tree.java new file mode 100644 index 0000000..ae7deaf --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/entity/Tree.java @@ -0,0 +1,43 @@ +package com.chukapoka.server.tree.entity; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Data +@NoArgsConstructor +@Table(name = "tb_tree") +public class Tree { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "treeId") + private Long id; + @Column(name = "linkId", nullable = false) + private String linkId; + @Column(name = "sendId", nullable = false) + private String sendId; + @Column(name = "title", nullable = false) + private String title; + @Column(name = "updateAt", nullable = false) + private LocalDateTime updatedAt; + + @Builder + public Tree(Long id, String linkId, String sendId, String title, LocalDateTime updatedAt) { + this.id = id; + this.linkId = linkId; + this.sendId = sendId; + this.title = title; + this.updatedAt = updatedAt; + } + // private String type; +// private String treeBgColor; +// private String groundColor; +// private String treeTopColor; +// private String treeItemColor; +// private String treeBottomColor; + +} diff --git a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java new file mode 100644 index 0000000..cdabcb5 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java @@ -0,0 +1,12 @@ +package com.chukapoka.server.tree.repository; + +import com.chukapoka.server.tree.entity.Tree; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface TreeRepository extends JpaRepository { + Tree findById(Long treeId); +} diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeService.java b/src/main/java/com/chukapoka/server/tree/service/TreeService.java new file mode 100644 index 0000000..7069204 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/service/TreeService.java @@ -0,0 +1,17 @@ +package com.chukapoka.server.tree.service; + +import com.chukapoka.server.tree.entity.Tree; + +import java.util.List; + +public interface TreeService { + + // 회원 아이디로 트리 아이템 저장 + Tree createTree( Tree tree); + + // 트리 리스트 조회 + List TreeList(); + + // 트리 삭제 + void deleteTree(String treeId); +} diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java new file mode 100644 index 0000000..58cab48 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -0,0 +1,33 @@ +package com.chukapoka.server.tree.service; + +import com.chukapoka.server.tree.entity.Tree; +import com.chukapoka.server.tree.repository.TreeRepository; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@AllArgsConstructor +public class TreeServiceImpl implements TreeService{ + + private final TreeRepository treeRepository; + + /** 트리생성 */ + @Override + public Tree createTree(Tree tree) { + return null; + } + + /** 사용자 트리 리스트 */ + @Override + public List TreeList() { + return null; + } + + /** 트리 삭제 */ + @Override + public void deleteTree(String treeId) { + + } +} diff --git a/src/main/java/com/chukapoka/server/user/dto/UserResponseDto.java b/src/main/java/com/chukapoka/server/user/dto/UserResponseDto.java index 0d63ba0..edeed49 100644 --- a/src/main/java/com/chukapoka/server/user/dto/UserResponseDto.java +++ b/src/main/java/com/chukapoka/server/user/dto/UserResponseDto.java @@ -21,9 +21,4 @@ public class UserResponseDto { private Long userId; // unique_userid private TokenResponseDto token; // JWT 토큰 - public UserResponseDto(ResultType result, String email, Long userId) { - this.result = result; - this.email = email; - this.userId = userId; - } } diff --git a/src/main/java/com/chukapoka/server/user/repository/UserRepository.java b/src/main/java/com/chukapoka/server/user/repository/UserRepository.java index 9853b04..4c8a2e3 100644 --- a/src/main/java/com/chukapoka/server/user/repository/UserRepository.java +++ b/src/main/java/com/chukapoka/server/user/repository/UserRepository.java @@ -20,8 +20,7 @@ public interface UserRepository extends JpaRepository { Optional findByEmailAndEmailType(String email, String emailType); - @Override - Optional findById(Long aLong); + Optional findById(Long id); // 이메일이 등록되어있는지 이메일과 이메일타입 확인 boolean existsByEmailAndEmailType(String email, String emailType); From 30cee385c280ba8a419f28b74c23c2f1398563d4 Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Thu, 22 Feb 2024 22:55:49 +0900 Subject: [PATCH 02/25] =?UTF-8?q?=ED=8A=B8=EB=A6=AC=EA=B0=80=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EC=96=B4=EC=A7=80=EA=B3=A0=20=EC=96=B4=EB=96=A4=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=ED=95=B4=EC=95=BC=ED=95=A0=EC=A7=80...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chukapoka/server/tree/entity/Tree.java | 60 +++++++++++++------ .../tree/repository/TreeRepository.java | 3 +- .../server/tree/service/TreeService.java | 16 +++-- .../server/tree/service/TreeServiceImpl.java | 24 +++++++- src/main/resources/application.yaml | 4 +- 5 files changed, 76 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/chukapoka/server/tree/entity/Tree.java b/src/main/java/com/chukapoka/server/tree/entity/Tree.java index ae7deaf..8f8a07c 100644 --- a/src/main/java/com/chukapoka/server/tree/entity/Tree.java +++ b/src/main/java/com/chukapoka/server/tree/entity/Tree.java @@ -1,6 +1,8 @@ package com.chukapoka.server.tree.entity; import jakarta.persistence.*; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @@ -9,6 +11,7 @@ @Entity @Data +@AllArgsConstructor @NoArgsConstructor @Table(name = "tb_tree") public class Tree { @@ -16,28 +19,47 @@ public class Tree { @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "treeId") private Long id; - @Column(name = "linkId", nullable = false) - private String linkId; - @Column(name = "sendId", nullable = false) - private String sendId; + + /** 트리제목 */ @Column(name = "title", nullable = false) private String title; - @Column(name = "updateAt", nullable = false) + + /** 내트리 || 미부여 트리 */ + @Column(name = "type", nullable = false) + private String type; + + /** 트리 링크를 특정하기 위한 id*/ + @Column(name = "linkId", nullable = false, unique = true, length = 200) + private String linkId; + + /** 타인에게 트리를 전달할 때 트리를 특정하기 위한 id */ + @Column(name = "sendId", unique = true, length = 200) + private String sendId; + + /** 트라 관련 색상은 String -> enum type으로 상수로 바꿔야 관리가 더 편할것같음 */ + @Column(name = "treeBgColor", nullable = true) + private String treeBgColor; + + @Column(name = "groundColor", nullable = true) + private String groundColor; + + @Column(name = "treeTopColor", nullable = true) + private String treeTopColor; + + @Column(name = "treeItemColor", nullable = true) + private String treeItemColor; + + @Column(name = "treeBottomColor", nullable = true) + private String treeBottomColor; + + /** userId가 값임 */ + @Column(name = "updatedBy", nullable = false) + private String updatedBy; + /** 생성 시간 */ + @Column(name = "updatedAt", nullable = false) private LocalDateTime updatedAt; - @Builder - public Tree(Long id, String linkId, String sendId, String title, LocalDateTime updatedAt) { - this.id = id; - this.linkId = linkId; - this.sendId = sendId; - this.title = title; - this.updatedAt = updatedAt; - } - // private String type; -// private String treeBgColor; -// private String groundColor; -// private String treeTopColor; -// private String treeItemColor; -// private String treeBottomColor; + + } diff --git a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java index cdabcb5..05dd403 100644 --- a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java +++ b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java @@ -4,9 +4,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.util.List; - @Repository public interface TreeRepository extends JpaRepository { Tree findById(Long treeId); + void delete(Long treeId); } diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeService.java b/src/main/java/com/chukapoka/server/tree/service/TreeService.java index 7069204..7559302 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeService.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeService.java @@ -6,12 +6,18 @@ public interface TreeService { - // 회원 아이디로 트리 아이템 저장 - Tree createTree( Tree tree); + /** 트리 저장 */ + Tree createTree(Tree tree); - // 트리 리스트 조회 + /** 트리리스트 조회(리스트용 모델) */ List TreeList(); - // 트리 삭제 - void deleteTree(String treeId); + /** + * 트리 상세 정보 조회 (상세정보 모델) + */ + Tree TreeDetail(Long treeId); + + /** 트리 삭제 */ + void deleteTree(Long treeId); + } diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index 58cab48..b9ffaec 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -2,7 +2,11 @@ import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.repository.TreeRepository; +import com.chukapoka.server.user.repository.UserRepository; import lombok.AllArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import java.util.List; @@ -13,21 +17,35 @@ public class TreeServiceImpl implements TreeService{ private final TreeRepository treeRepository; + /** 트리생성 */ @Override public Tree createTree(Tree tree) { - return null; + + return treeRepository.save(tree); } - /** 사용자 트리 리스트 */ + /** 사용자 트리 리스트 조화(리스트용 모델) */ @Override public List TreeList() { return null; } + /** 트리 상세 정보 조회 (상세정보 모델) */ + @Override + public Tree TreeDetail(Long treeId) { + return null; + } + /** 트리 삭제 */ @Override - public void deleteTree(String treeId) { + public void deleteTree(Long treeId) { + treeRepository.delete(treeId); + } + /** 사용자 ID 반환 메서드 */ + private String getCurrentUserId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication.getName(); } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index e401720..b0a481e 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,6 +1,6 @@ spring: profiles: # active: prod - active: dev +# active: dev # active: dev-db -# active: local + active: local \ No newline at end of file From edc93790c00a813a6ee88412d85c49c74325f696 Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Fri, 23 Feb 2024 20:53:26 +0900 Subject: [PATCH 03/25] =?UTF-8?q?=ED=8A=B8=EB=A6=AC=20=EC=83=9D=EC=84=B1,?= =?UTF-8?q?=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C,=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20-=20createTree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/authority/SecurityConfig.java | 3 +- .../server/common/enums/TreeType.java | 8 +++++ .../tree/controller/TreeController.java | 36 +++++++++++++++++-- .../chukapoka/server/tree/entity/Tree.java | 22 +++++++----- .../tree/repository/TreeRepository.java | 12 +++++-- .../server/tree/service/TreeService.java | 8 ++--- .../server/tree/service/TreeServiceImpl.java | 25 ++++++------- .../user/repository/UserRepository.java | 1 + src/main/resources/application.yaml | 4 +-- 9 files changed, 82 insertions(+), 37 deletions(-) create mode 100644 src/main/java/com/chukapoka/server/common/enums/TreeType.java diff --git a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java index bfb6556..b449f4a 100644 --- a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java +++ b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java @@ -24,7 +24,6 @@ public class SecurityConfig { */ @Autowired private JwtTokenProvider jwtTokenProvider; - @Autowired private TokenRepository tokenRepository; @@ -38,7 +37,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { authorizeRequests .requestMatchers("/api/user/emailCheck", "/api/user", "/api/user/authNumber", "/api/health").anonymous() - .requestMatchers("/api/user/logout", "api/user/reissue").hasRole(Authority.USER.getAuthority());// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 + .requestMatchers("/api/user/logout", "api/user/reissue","/api/tree").hasRole(Authority.USER.getAuthority());// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 } diff --git a/src/main/java/com/chukapoka/server/common/enums/TreeType.java b/src/main/java/com/chukapoka/server/common/enums/TreeType.java new file mode 100644 index 0000000..9164a70 --- /dev/null +++ b/src/main/java/com/chukapoka/server/common/enums/TreeType.java @@ -0,0 +1,8 @@ +package com.chukapoka.server.common.enums; + +public enum TreeType { + MINE, + NOT_YET_SEND; + + +} diff --git a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java index 6de5ca4..1fc4e77 100644 --- a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java +++ b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java @@ -1,10 +1,42 @@ package com.chukapoka.server.tree.controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import com.chukapoka.server.common.dto.BaseResponse; +import com.chukapoka.server.common.enums.ResultType; +import com.chukapoka.server.tree.entity.Tree; +import com.chukapoka.server.tree.service.TreeService; +import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; @RestController +@AllArgsConstructor @RequestMapping("/api/tree") public class TreeController { + + @Autowired + private final TreeService treeService ; + + /**트리 생성 */ + @PostMapping + public BaseResponsecreateTree(@RequestBody Tree tree) { + Tree response = treeService.createTree(tree); + return new BaseResponse<>(ResultType.SUCCESS, response); + } + + /** 트리리스트 목록 */ + @GetMapping + private BaseResponse> treeList(@RequestParam("id") Long userId) { + List response = treeService.treeList(userId); + return new BaseResponse<>(ResultType.SUCCESS, response); + } + + /** 트리 삭제 */ + @DeleteMapping + private BaseResponse deleteTree(@RequestParam("treeId") Long treeId) { + treeService.deleteTree(treeId); + return new BaseResponse<>(ResultType.SUCCESS, null); + } } diff --git a/src/main/java/com/chukapoka/server/tree/entity/Tree.java b/src/main/java/com/chukapoka/server/tree/entity/Tree.java index 8f8a07c..816c0cc 100644 --- a/src/main/java/com/chukapoka/server/tree/entity/Tree.java +++ b/src/main/java/com/chukapoka/server/tree/entity/Tree.java @@ -1,11 +1,12 @@ package com.chukapoka.server.tree.entity; +import com.chukapoka.server.common.enums.TreeType; + import jakarta.persistence.*; -import jakarta.validation.Valid; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.springframework.data.annotation.LastModifiedDate; import java.time.LocalDateTime; @@ -18,15 +19,16 @@ public class Tree { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "treeId") - private Long id; + private Long treeId; /** 트리제목 */ @Column(name = "title", nullable = false) private String title; /** 내트리 || 미부여 트리 */ + @Enumerated(EnumType.STRING) @Column(name = "type", nullable = false) - private String type; + private TreeType type; /** 트리 링크를 특정하기 위한 id*/ @Column(name = "linkId", nullable = false, unique = true, length = 200) @@ -53,13 +55,17 @@ public class Tree { private String treeBottomColor; /** userId가 값임 */ - @Column(name = "updatedBy", nullable = false) - private String updatedBy; + @Column(name = "updatedBy") + private Long updatedBy; + /** 생성 시간 */ @Column(name = "updatedAt", nullable = false) + @LastModifiedDate private LocalDateTime updatedAt; - - + @PrePersist + public void updatedAt() { + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java index 05dd403..a3bb86d 100644 --- a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java +++ b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java @@ -1,11 +1,17 @@ package com.chukapoka.server.tree.repository; import com.chukapoka.server.tree.entity.Tree; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + @Repository -public interface TreeRepository extends JpaRepository { - Tree findById(Long treeId); - void delete(Long treeId); +public interface TreeRepository extends JpaRepository { + Optional findById(Long treeId); + + List findByUpdatedBy(Long userId); } diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeService.java b/src/main/java/com/chukapoka/server/tree/service/TreeService.java index 7559302..e450277 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeService.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeService.java @@ -10,12 +10,10 @@ public interface TreeService { Tree createTree(Tree tree); /** 트리리스트 조회(리스트용 모델) */ - List TreeList(); + List treeList(Long userId); - /** - * 트리 상세 정보 조회 (상세정보 모델) - */ - Tree TreeDetail(Long treeId); + /** 트리 상세 정보 조회 (상세정보 모델) */ + Tree treeDetail(Long treeId); /** 트리 삭제 */ void deleteTree(Long treeId); diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index b9ffaec..ce1ca95 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -1,12 +1,10 @@ package com.chukapoka.server.tree.service; +import com.chukapoka.server.common.dto.CustomUser; import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.repository.TreeRepository; -import com.chukapoka.server.user.repository.UserRepository; import lombok.AllArgsConstructor; -import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import java.util.List; @@ -17,35 +15,32 @@ public class TreeServiceImpl implements TreeService{ private final TreeRepository treeRepository; - /** 트리생성 */ @Override public Tree createTree(Tree tree) { - + long userId = ((CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + tree.setUpdatedBy(userId); return treeRepository.save(tree); } - /** 사용자 트리 리스트 조화(리스트용 모델) */ + /**사용자 트리 리스트 조화(리스트용 모델) */ @Override - public List TreeList() { - return null; + public List treeList(Long userId) { + return treeRepository.findByUpdatedBy(userId); } /** 트리 상세 정보 조회 (상세정보 모델) */ @Override - public Tree TreeDetail(Long treeId) { + public Tree treeDetail(Long treeId) { return null; } /** 트리 삭제 */ @Override public void deleteTree(Long treeId) { - treeRepository.delete(treeId); + treeRepository.deleteById(treeId); } - /** 사용자 ID 반환 메서드 */ - private String getCurrentUserId() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return authentication.getName(); - } + + } diff --git a/src/main/java/com/chukapoka/server/user/repository/UserRepository.java b/src/main/java/com/chukapoka/server/user/repository/UserRepository.java index 4c8a2e3..6c8bedb 100644 --- a/src/main/java/com/chukapoka/server/user/repository/UserRepository.java +++ b/src/main/java/com/chukapoka/server/user/repository/UserRepository.java @@ -25,4 +25,5 @@ public interface UserRepository extends JpaRepository { // 이메일이 등록되어있는지 이메일과 이메일타입 확인 boolean existsByEmailAndEmailType(String email, String emailType); + } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index b0a481e..462daff 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,6 +1,6 @@ spring: profiles: # active: prod -# active: dev + active: dev # active: dev-db - active: local \ No newline at end of file +# active: local \ No newline at end of file From 28be02e736712b6ffd983e3c7cde9bdf768e1e98 Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Sat, 24 Feb 2024 23:21:01 +0900 Subject: [PATCH 04/25] =?UTF-8?q?-=20creatTree=20=20=20-=20TreeRequestDto?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20@Valid=EA=B2=80=EC=82=AC=20?= =?UTF-8?q?-=20TreeList=20=20=20-=20TreeListResponseDto=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=20=20-=20List=EB=8D=B0=EC=9D=B4=ED=84=B0=EA=B0=80=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EB=90=A0=EC=88=98=20=EC=9E=88=EA=B8=B0?= =?UTF-8?q?=EB=95=8C=EB=AC=B8=EC=97=90=20TreeList=20=EC=B6=94=EA=B0=80=20-?= =?UTF-8?q?=20deleteTree=20=20=20-=20@PathVariable=20treeId=EA=B0=92?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=8A=B8=EB=A6=AC=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/authority/SecurityConfig.java | 2 +- .../server/common/enums/TreeType.java | 26 +++++++++++++++-- .../tree/controller/TreeController.java | 20 ++++++++----- .../chukapoka/server/tree/dto/TreeList.java | 25 ++++++++++++++++ .../server/tree/dto/TreeListResponseDto.java | 22 ++++++++++++++ .../server/tree/dto/TreeRequestDto.java | 26 +++++++++++++++++ .../chukapoka/server/tree/entity/Tree.java | 19 ++++++------ .../tree/repository/TreeRepository.java | 8 +++-- .../server/tree/service/TreeService.java | 6 ++-- .../server/tree/service/TreeServiceImpl.java | 29 +++++++++++++++---- 10 files changed, 154 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/chukapoka/server/tree/dto/TreeList.java create mode 100644 src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java create mode 100644 src/main/java/com/chukapoka/server/tree/dto/TreeRequestDto.java diff --git a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java index b449f4a..bb3d47e 100644 --- a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java +++ b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java @@ -37,7 +37,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { authorizeRequests .requestMatchers("/api/user/emailCheck", "/api/user", "/api/user/authNumber", "/api/health").anonymous() - .requestMatchers("/api/user/logout", "api/user/reissue","/api/tree").hasRole(Authority.USER.getAuthority());// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 + .requestMatchers("/api/user/logout", "api/user/reissue","/api/tree","api/tree/**").hasRole(Authority.USER.getAuthority());// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 } diff --git a/src/main/java/com/chukapoka/server/common/enums/TreeType.java b/src/main/java/com/chukapoka/server/common/enums/TreeType.java index 9164a70..f95fa85 100644 --- a/src/main/java/com/chukapoka/server/common/enums/TreeType.java +++ b/src/main/java/com/chukapoka/server/common/enums/TreeType.java @@ -1,8 +1,30 @@ package com.chukapoka.server.common.enums; +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +@Getter public enum TreeType { - MINE, - NOT_YET_SEND; + MINE("내트리"), + NOT_YET_SEND("미부여 트리"); + + private final String description; + private static final Map lookup = new HashMap<>(); + + static { + for (TreeType treeType : TreeType.values()) { + lookup.put(treeType.getDescription(), treeType); + } + } + + TreeType(String description) { + this.description = description; + } + public static TreeType getByDescription(String description) { + return lookup.get(description); + } } diff --git a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java index 1fc4e77..9bbdab3 100644 --- a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java +++ b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java @@ -2,8 +2,11 @@ import com.chukapoka.server.common.dto.BaseResponse; import com.chukapoka.server.common.enums.ResultType; +import com.chukapoka.server.tree.dto.TreeListResponseDto; +import com.chukapoka.server.tree.dto.TreeRequestDto; import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.service.TreeService; +import jakarta.validation.Valid; import lombok.AllArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -20,21 +23,22 @@ public class TreeController { /**트리 생성 */ @PostMapping - public BaseResponsecreateTree(@RequestBody Tree tree) { - Tree response = treeService.createTree(tree); - return new BaseResponse<>(ResultType.SUCCESS, response); + public BaseResponsecreateTree(@Valid @RequestBody TreeRequestDto treeRequestDto) { + Tree responseDto = treeService.createTree(treeRequestDto); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); } /** 트리리스트 목록 */ @GetMapping - private BaseResponse> treeList(@RequestParam("id") Long userId) { - List response = treeService.treeList(userId); - return new BaseResponse<>(ResultType.SUCCESS, response); + public BaseResponse treeList() { + TreeListResponseDto responseDto = treeService.treeList(); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); } + /** 트리 삭제 */ - @DeleteMapping - private BaseResponse deleteTree(@RequestParam("treeId") Long treeId) { + @DeleteMapping("/{treeId}") + public BaseResponse deleteTree(@PathVariable("treeId") Long treeId) { treeService.deleteTree(treeId); return new BaseResponse<>(ResultType.SUCCESS, null); } diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeList.java b/src/main/java/com/chukapoka/server/tree/dto/TreeList.java new file mode 100644 index 0000000..db0b6e5 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeList.java @@ -0,0 +1,25 @@ +package com.chukapoka.server.tree.dto; + +import com.chukapoka.server.common.enums.TreeType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TreeList { + + private Long treeId; + private String title; + private String type; // MINE or NOT_YEN_SEND + private String linkId; + private String sendId; + private Long updatedBy; + private LocalDateTime updatedAt; + + /** 트리색상 부분 추가 가능 */ + +} diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java new file mode 100644 index 0000000..1fb57af --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java @@ -0,0 +1,22 @@ +package com.chukapoka.server.tree.dto; + + + + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TreeListResponseDto { + + /** 트리 리스트 목록 */ + private List trees; +} + + diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeRequestDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeRequestDto.java new file mode 100644 index 0000000..e20599f --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeRequestDto.java @@ -0,0 +1,26 @@ +package com.chukapoka.server.tree.dto; + +import com.chukapoka.server.common.annotation.ValidEnum; +import com.chukapoka.server.common.enums.TreeType; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class TreeRequestDto { + @NotBlank(message = "title is null") + private String title; + @NotNull(message = "treeType is null") + @ValidEnum(enumClass = TreeType.class, message = "TreeType must be MINE or NOT_YET_SEND") + private String type; + @NotBlank(message = "linkId is null") + private String linkId; + @NotBlank(message = "sendId is null") + private String sendId; + private Long updatedBy; // 클라이언트에서는 입력받을 필요없음 + private String treeBgColor; + private String groundColor; + private String treeTopColor; + private String treeItemColor; + private String treeBottomColor; +} \ No newline at end of file diff --git a/src/main/java/com/chukapoka/server/tree/entity/Tree.java b/src/main/java/com/chukapoka/server/tree/entity/Tree.java index 816c0cc..1a75822 100644 --- a/src/main/java/com/chukapoka/server/tree/entity/Tree.java +++ b/src/main/java/com/chukapoka/server/tree/entity/Tree.java @@ -26,9 +26,8 @@ public class Tree { private String title; /** 내트리 || 미부여 트리 */ - @Enumerated(EnumType.STRING) @Column(name = "type", nullable = false) - private TreeType type; + private String type; /** 트리 링크를 특정하기 위한 id*/ @Column(name = "linkId", nullable = false, unique = true, length = 200) @@ -38,6 +37,15 @@ public class Tree { @Column(name = "sendId", unique = true, length = 200) private String sendId; + /** userId가 값임 */ + @Column(name = "updatedBy") + private Long updatedBy; + + /** 생성 시간 */ + @Column(name = "updatedAt", nullable = false) + @LastModifiedDate + private LocalDateTime updatedAt; + /** 트라 관련 색상은 String -> enum type으로 상수로 바꿔야 관리가 더 편할것같음 */ @Column(name = "treeBgColor", nullable = true) private String treeBgColor; @@ -54,14 +62,7 @@ public class Tree { @Column(name = "treeBottomColor", nullable = true) private String treeBottomColor; - /** userId가 값임 */ - @Column(name = "updatedBy") - private Long updatedBy; - /** 생성 시간 */ - @Column(name = "updatedAt", nullable = false) - @LastModifiedDate - private LocalDateTime updatedAt; @PrePersist public void updatedAt() { diff --git a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java index a3bb86d..0d1713a 100644 --- a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java +++ b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java @@ -1,17 +1,21 @@ package com.chukapoka.server.tree.repository; + +import com.chukapoka.server.tree.dto.TreeList; import com.chukapoka.server.tree.entity.Tree; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; - import java.util.List; import java.util.Optional; @Repository public interface TreeRepository extends JpaRepository { Optional findById(Long treeId); + @Query("SELECT new com.chukapoka.server.tree.dto.TreeList(tree.treeId, tree.title, tree.type, tree.linkId, tree.sendId, tree.updatedBy, tree.updatedAt) FROM Tree tree") + List findAllTrees(); + - List findByUpdatedBy(Long userId); } diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeService.java b/src/main/java/com/chukapoka/server/tree/service/TreeService.java index e450277..ddf87b9 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeService.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeService.java @@ -1,5 +1,7 @@ package com.chukapoka.server.tree.service; +import com.chukapoka.server.tree.dto.TreeListResponseDto; +import com.chukapoka.server.tree.dto.TreeRequestDto; import com.chukapoka.server.tree.entity.Tree; import java.util.List; @@ -7,10 +9,10 @@ public interface TreeService { /** 트리 저장 */ - Tree createTree(Tree tree); + Tree createTree(TreeRequestDto treeRequestDto); /** 트리리스트 조회(리스트용 모델) */ - List treeList(Long userId); + TreeListResponseDto treeList(); /** 트리 상세 정보 조회 (상세정보 모델) */ Tree treeDetail(Long treeId); diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index ce1ca95..e39b8a0 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -1,12 +1,19 @@ package com.chukapoka.server.tree.service; import com.chukapoka.server.common.dto.CustomUser; +import com.chukapoka.server.tree.dto.TreeList; +import com.chukapoka.server.tree.dto.TreeListResponseDto; +import com.chukapoka.server.tree.dto.TreeRequestDto; import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.repository.TreeRepository; +import jakarta.persistence.EntityNotFoundException; import lombok.AllArgsConstructor; +import org.springframework.beans.BeanUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; @Service @@ -15,18 +22,27 @@ public class TreeServiceImpl implements TreeService{ private final TreeRepository treeRepository; + /** 트리생성 */ @Override - public Tree createTree(Tree tree) { + @Transactional + public Tree createTree(TreeRequestDto treeRequestDto) { long userId = ((CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); - tree.setUpdatedBy(userId); + // 클라이언트에서 입력 받을 필요없이 토큰으로 접속후 권한id로 셋팅 + treeRequestDto.setUpdatedBy(userId); + Tree tree = new Tree(); + BeanUtils.copyProperties(treeRequestDto, tree); // mapper대신 사용할수 있지만 복사할 속성의 수가 적고 속성 이름이 일치하는 경우에 적합 return treeRepository.save(tree); } - /**사용자 트리 리스트 조화(리스트용 모델) */ + /** 사용자 트리 리스트 조회(리스트용 모델) */ @Override - public List treeList(Long userId) { - return treeRepository.findByUpdatedBy(userId); + public TreeListResponseDto treeList() { + List trees = treeRepository.findAllTrees(); + if (trees.isEmpty()) { + return new TreeListResponseDto(new ArrayList<>()); + } + return new TreeListResponseDto(trees); } /** 트리 상세 정보 조회 (상세정보 모델) */ @@ -37,7 +53,10 @@ public Tree treeDetail(Long treeId) { /** 트리 삭제 */ @Override + @Transactional public void deleteTree(Long treeId) { + treeRepository.findById(treeId) + .orElseThrow(() -> new EntityNotFoundException("등록되지 않은 " + treeId + "입니다.")); treeRepository.deleteById(treeId); } From a222825e14162c374aedc63df87472b7e0eda25b Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Sun, 25 Feb 2024 11:32:02 +0900 Subject: [PATCH 05/25] =?UTF-8?q?treeModify=20=ED=8A=B8=EB=A6=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EB=B6=80=EB=B6=84=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tree/controller/TreeController.java | 12 ++++++- .../server/tree/dto/TreeListResponseDto.java | 2 +- .../server/tree/dto/TreeModifyRequestDto.java | 14 ++++++++ .../chukapoka/server/tree/entity/Tree.java | 8 +++-- .../server/tree/service/TreeService.java | 6 ++-- .../server/tree/service/TreeServiceImpl.java | 32 ++++++++++++++++--- 6 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java diff --git a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java index 9bbdab3..2f765bb 100644 --- a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java +++ b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java @@ -4,6 +4,7 @@ import com.chukapoka.server.common.enums.ResultType; import com.chukapoka.server.tree.dto.TreeListResponseDto; import com.chukapoka.server.tree.dto.TreeRequestDto; +import com.chukapoka.server.tree.dto.TreeModifyRequestDto; import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.service.TreeService; import jakarta.validation.Valid; @@ -11,7 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; -import java.util.List; + @RestController @AllArgsConstructor @@ -35,6 +36,14 @@ public BaseResponse treeList() { return new BaseResponse<>(ResultType.SUCCESS, responseDto); } + /** 트리 수정 */ + @PutMapping("/{treeId}") + public BaseResponse treeModify(@PathVariable("treeId") Long treeId, + @RequestBody TreeModifyRequestDto treeModifyDto) { + System.out.println("treeModifyDto = " + treeModifyDto); + Tree responseDto = treeService.treeModify(treeId, treeModifyDto); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); + } /** 트리 삭제 */ @DeleteMapping("/{treeId}") @@ -42,5 +51,6 @@ public BaseResponse deleteTree(@PathVariable("treeId") Long treeId) { treeService.deleteTree(treeId); return new BaseResponse<>(ResultType.SUCCESS, null); } + } diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java index 1fb57af..5aa70f7 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java @@ -16,7 +16,7 @@ public class TreeListResponseDto { /** 트리 리스트 목록 */ - private List trees; + private List treeList; } diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java new file mode 100644 index 0000000..5dc9f44 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java @@ -0,0 +1,14 @@ +package com.chukapoka.server.tree.dto; + +import lombok.Data; + +@Data +public class TreeModifyRequestDto { + private String title; + private String type; + private String treeBgColor; + private String groundColor; + private String treeTopColor; + private String treeItemColor; + private String treeBottomColor; +} diff --git a/src/main/java/com/chukapoka/server/tree/entity/Tree.java b/src/main/java/com/chukapoka/server/tree/entity/Tree.java index 1a75822..c37c066 100644 --- a/src/main/java/com/chukapoka/server/tree/entity/Tree.java +++ b/src/main/java/com/chukapoka/server/tree/entity/Tree.java @@ -1,11 +1,12 @@ package com.chukapoka.server.tree.entity; -import com.chukapoka.server.common.enums.TreeType; import jakarta.persistence.*; + import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicUpdate; import org.springframework.data.annotation.LastModifiedDate; import java.time.LocalDateTime; @@ -14,6 +15,7 @@ @Data @AllArgsConstructor @NoArgsConstructor +@DynamicUpdate // 데이터의 변경사항이 있는 것만 수정 @Table(name = "tb_tree") public class Tree { @Id @@ -22,11 +24,11 @@ public class Tree { private Long treeId; /** 트리제목 */ - @Column(name = "title", nullable = false) + @Column(name = "title") private String title; /** 내트리 || 미부여 트리 */ - @Column(name = "type", nullable = false) + @Column(name = "type") private String type; /** 트리 링크를 특정하기 위한 id*/ diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeService.java b/src/main/java/com/chukapoka/server/tree/service/TreeService.java index ddf87b9..68eacc0 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeService.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeService.java @@ -2,10 +2,9 @@ import com.chukapoka.server.tree.dto.TreeListResponseDto; import com.chukapoka.server.tree.dto.TreeRequestDto; +import com.chukapoka.server.tree.dto.TreeModifyRequestDto; import com.chukapoka.server.tree.entity.Tree; -import java.util.List; - public interface TreeService { /** 트리 저장 */ @@ -17,6 +16,9 @@ public interface TreeService { /** 트리 상세 정보 조회 (상세정보 모델) */ Tree treeDetail(Long treeId); + /** 트리 수정 */ + Tree treeModify(Long treeId, TreeModifyRequestDto treeModifyDto); + /** 트리 삭제 */ void deleteTree(Long treeId); diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index e39b8a0..56b163c 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -4,10 +4,12 @@ import com.chukapoka.server.tree.dto.TreeList; import com.chukapoka.server.tree.dto.TreeListResponseDto; import com.chukapoka.server.tree.dto.TreeRequestDto; +import com.chukapoka.server.tree.dto.TreeModifyRequestDto; import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.repository.TreeRepository; import jakarta.persistence.EntityNotFoundException; import lombok.AllArgsConstructor; +import org.modelmapper.ModelMapper; import org.springframework.beans.BeanUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; @@ -21,16 +23,16 @@ public class TreeServiceImpl implements TreeService{ private final TreeRepository treeRepository; - + private final ModelMapper modelMapper; /** 트리생성 */ @Override @Transactional public Tree createTree(TreeRequestDto treeRequestDto) { - long userId = ((CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + Tree tree = new Tree(); // 클라이언트에서 입력 받을 필요없이 토큰으로 접속후 권한id로 셋팅 + long userId = ((CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); treeRequestDto.setUpdatedBy(userId); - Tree tree = new Tree(); BeanUtils.copyProperties(treeRequestDto, tree); // mapper대신 사용할수 있지만 복사할 속성의 수가 적고 속성 이름이 일치하는 경우에 적합 return treeRepository.save(tree); } @@ -48,9 +50,22 @@ public TreeListResponseDto treeList() { /** 트리 상세 정보 조회 (상세정보 모델) */ @Override public Tree treeDetail(Long treeId) { + return null; } + /** 트리수정 */ + @Override + @Transactional // 데이터 변경감지 + public Tree treeModify(Long treeId, TreeModifyRequestDto treeModifyDto) { + // 트리 아이디로 트리를 찾음 + Tree tree = treeRepository.findById(treeId) + .orElseThrow(() -> new EntityNotFoundException("treeId " + treeId + " 를 찾을 수 없습니다.")); + updateTreeAttributes(tree, treeModifyDto); + return treeRepository.save(tree); + + } + /** 트리 삭제 */ @Override @Transactional @@ -61,5 +76,14 @@ public void deleteTree(Long treeId) { } - + /** 트리필드 정보 업데이트 */ + private void updateTreeAttributes(Tree tree, TreeModifyRequestDto treeModifyDto) { + tree.setTitle(treeModifyDto.getTitle()); + tree.setType(treeModifyDto.getType()); + tree.setTreeBgColor(treeModifyDto.getTreeBgColor()); + tree.setGroundColor(treeModifyDto.getGroundColor()); + tree.setTreeTopColor(treeModifyDto.getTreeTopColor()); + tree.setTreeItemColor(treeModifyDto.getTreeItemColor()); + tree.setTreeBottomColor(treeModifyDto.getTreeBottomColor()); + } } From 283436158c935a4adcb940dc934c80fc1d41f4cc Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Sun, 25 Feb 2024 12:37:18 +0900 Subject: [PATCH 06/25] =?UTF-8?q?-=20Tree=EC=83=81=EC=84=B8=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=20=20=20-=20TreeDetailResponseDto=20=EC=B6=94=EA=B0=80=20(tree?= =?UTF-8?q?item=20=ED=95=84=EB=93=9C=20=ED=95=84=EC=9A=94)=20-=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EC=9C=84=ED=95=9C=20mod?= =?UTF-8?q?elMapper=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 ++ .../server/common/authority/AppConfig.java | 15 +++++++++ .../tree/controller/TreeController.java | 18 +++++++--- ...uestDto.java => TreeCreateRequestDto.java} | 4 +-- .../tree/dto/TreeDetailResponseDto.java | 33 +++++++++++++++++++ .../chukapoka/server/tree/dto/TreeList.java | 2 +- .../server/tree/dto/TreeListResponseDto.java | 5 --- .../server/tree/dto/TreeModifyRequestDto.java | 3 ++ .../chukapoka/server/tree/entity/Tree.java | 19 ++++++----- .../tree/repository/TreeRepository.java | 1 + .../server/tree/service/TreeService.java | 9 ++--- 11 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/chukapoka/server/common/authority/AppConfig.java rename src/main/java/com/chukapoka/server/tree/dto/{TreeRequestDto.java => TreeCreateRequestDto.java} (89%) create mode 100644 src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java diff --git a/build.gradle b/build.gradle index 485b8ef..a94d31d 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,8 @@ dependencies { // enable production implementation ("org.springframework.boot:spring-boot-starter-actuator") + // modelmapper + implementation 'org.modelmapper:modelmapper:2.4.4' } diff --git a/src/main/java/com/chukapoka/server/common/authority/AppConfig.java b/src/main/java/com/chukapoka/server/common/authority/AppConfig.java new file mode 100644 index 0000000..cc95cde --- /dev/null +++ b/src/main/java/com/chukapoka/server/common/authority/AppConfig.java @@ -0,0 +1,15 @@ +package com.chukapoka.server.common.authority; + + +import org.modelmapper.ModelMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AppConfig { + + @Bean + public ModelMapper modelMapper() { + return new ModelMapper(); + } +} diff --git a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java index 2f765bb..e5c9ccb 100644 --- a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java +++ b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java @@ -2,8 +2,9 @@ import com.chukapoka.server.common.dto.BaseResponse; import com.chukapoka.server.common.enums.ResultType; +import com.chukapoka.server.tree.dto.TreeDetailResponseDto; import com.chukapoka.server.tree.dto.TreeListResponseDto; -import com.chukapoka.server.tree.dto.TreeRequestDto; +import com.chukapoka.server.tree.dto.TreeCreateRequestDto; import com.chukapoka.server.tree.dto.TreeModifyRequestDto; import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.service.TreeService; @@ -24,7 +25,7 @@ public class TreeController { /**트리 생성 */ @PostMapping - public BaseResponsecreateTree(@Valid @RequestBody TreeRequestDto treeRequestDto) { + public BaseResponsecreateTree(@Valid @RequestBody TreeCreateRequestDto treeRequestDto) { Tree responseDto = treeService.createTree(treeRequestDto); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } @@ -36,19 +37,26 @@ public BaseResponse treeList() { return new BaseResponse<>(ResultType.SUCCESS, responseDto); } + /** 트리상세 정보 */ + @GetMapping("/{treeId}") + private BaseResponse treeDetail(@PathVariable("treeId") Long treeId) { + TreeDetailResponseDto responseDto = treeService.treeDetail(treeId); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); + } + /** 트리 수정 */ @PutMapping("/{treeId}") public BaseResponse treeModify(@PathVariable("treeId") Long treeId, @RequestBody TreeModifyRequestDto treeModifyDto) { - System.out.println("treeModifyDto = " + treeModifyDto); Tree responseDto = treeService.treeModify(treeId, treeModifyDto); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } + /** 트리 삭제 */ @DeleteMapping("/{treeId}") - public BaseResponse deleteTree(@PathVariable("treeId") Long treeId) { - treeService.deleteTree(treeId); + public BaseResponse treeDelete(@PathVariable("treeId") Long treeId) { + treeService.treeDelete(treeId); return new BaseResponse<>(ResultType.SUCCESS, null); } diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeRequestDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeCreateRequestDto.java similarity index 89% rename from src/main/java/com/chukapoka/server/tree/dto/TreeRequestDto.java rename to src/main/java/com/chukapoka/server/tree/dto/TreeCreateRequestDto.java index e20599f..cbf0c9d 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeRequestDto.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeCreateRequestDto.java @@ -7,7 +7,7 @@ import lombok.Data; @Data -public class TreeRequestDto { +public class TreeCreateRequestDto { @NotBlank(message = "title is null") private String title; @NotNull(message = "treeType is null") @@ -17,7 +17,7 @@ public class TreeRequestDto { private String linkId; @NotBlank(message = "sendId is null") private String sendId; - private Long updatedBy; // 클라이언트에서는 입력받을 필요없음 + private Long updatedBy; // 클라이언트에서는 입력받을 필요없음 ( TreeServicelmpl.createTree 에서 처리 ) private String treeBgColor; private String groundColor; private String treeTopColor; diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java new file mode 100644 index 0000000..37f0c64 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java @@ -0,0 +1,33 @@ +package com.chukapoka.server.tree.dto; + + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TreeDetailResponseDto { + + /** 트리상세 정보 */ + private Long treeId; + private String title; + private String type; // MINE or NOT_YEN_SEND + private String treeBgColor; + private String groundColor; + private String treeTopColor; + private String treeItemColor; + private String treeBottomColor; + + /** treeItem 목록 필요 */ + + private Long updatedBy; + private LocalDateTime updatedAt; + + + +} diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeList.java b/src/main/java/com/chukapoka/server/tree/dto/TreeList.java index db0b6e5..a31090e 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeList.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeList.java @@ -1,6 +1,5 @@ package com.chukapoka.server.tree.dto; -import com.chukapoka.server.common.enums.TreeType; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -12,6 +11,7 @@ @NoArgsConstructor public class TreeList { + /** 트리 리스트정보 */ private Long treeId; private String title; private String type; // MINE or NOT_YEN_SEND diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java index 5aa70f7..8947a13 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java @@ -1,9 +1,4 @@ package com.chukapoka.server.tree.dto; - - - - - import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java index 5dc9f44..4db3135 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java @@ -4,6 +4,8 @@ @Data public class TreeModifyRequestDto { + + /** 트리에 관련된 것만 수정할것인지 ?*/ private String title; private String type; private String treeBgColor; @@ -11,4 +13,5 @@ public class TreeModifyRequestDto { private String treeTopColor; private String treeItemColor; private String treeBottomColor; + } diff --git a/src/main/java/com/chukapoka/server/tree/entity/Tree.java b/src/main/java/com/chukapoka/server/tree/entity/Tree.java index c37c066..fc56ae9 100644 --- a/src/main/java/com/chukapoka/server/tree/entity/Tree.java +++ b/src/main/java/com/chukapoka/server/tree/entity/Tree.java @@ -39,15 +39,6 @@ public class Tree { @Column(name = "sendId", unique = true, length = 200) private String sendId; - /** userId가 값임 */ - @Column(name = "updatedBy") - private Long updatedBy; - - /** 생성 시간 */ - @Column(name = "updatedAt", nullable = false) - @LastModifiedDate - private LocalDateTime updatedAt; - /** 트라 관련 색상은 String -> enum type으로 상수로 바꿔야 관리가 더 편할것같음 */ @Column(name = "treeBgColor", nullable = true) private String treeBgColor; @@ -64,6 +55,15 @@ public class Tree { @Column(name = "treeBottomColor", nullable = true) private String treeBottomColor; + /** userId가 값임 */ + @Column(name = "updatedBy") + private Long updatedBy; + + /** 생성 시간 */ + @Column(name = "updatedAt", nullable = false) + @LastModifiedDate + private LocalDateTime updatedAt; + @PrePersist @@ -71,4 +71,5 @@ public void updatedAt() { this.updatedAt = LocalDateTime.now(); } + } diff --git a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java index 0d1713a..b381de8 100644 --- a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java +++ b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java @@ -1,6 +1,7 @@ package com.chukapoka.server.tree.repository; + import com.chukapoka.server.tree.dto.TreeList; import com.chukapoka.server.tree.entity.Tree; diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeService.java b/src/main/java/com/chukapoka/server/tree/service/TreeService.java index 68eacc0..61b81af 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeService.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeService.java @@ -1,25 +1,26 @@ package com.chukapoka.server.tree.service; +import com.chukapoka.server.tree.dto.TreeDetailResponseDto; import com.chukapoka.server.tree.dto.TreeListResponseDto; -import com.chukapoka.server.tree.dto.TreeRequestDto; +import com.chukapoka.server.tree.dto.TreeCreateRequestDto; import com.chukapoka.server.tree.dto.TreeModifyRequestDto; import com.chukapoka.server.tree.entity.Tree; public interface TreeService { /** 트리 저장 */ - Tree createTree(TreeRequestDto treeRequestDto); + Tree createTree(TreeCreateRequestDto treeRequestDto); /** 트리리스트 조회(리스트용 모델) */ TreeListResponseDto treeList(); /** 트리 상세 정보 조회 (상세정보 모델) */ - Tree treeDetail(Long treeId); + TreeDetailResponseDto treeDetail(Long treeId); /** 트리 수정 */ Tree treeModify(Long treeId, TreeModifyRequestDto treeModifyDto); /** 트리 삭제 */ - void deleteTree(Long treeId); + void treeDelete(Long treeId); } From 45c15c356d3f36ba78da0ec82a84bdcbfeb7ad9d Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Sun, 25 Feb 2024 12:37:26 +0900 Subject: [PATCH 07/25] =?UTF-8?q?-=20Tree=EC=83=81=EC=84=B8=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=20=20=20-=20TreeDetailResponseDto=20=EC=B6=94=EA=B0=80=20(tree?= =?UTF-8?q?item=20=ED=95=84=EB=93=9C=20=ED=95=84=EC=9A=94)=20-=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EC=9C=84=ED=95=9C=20mod?= =?UTF-8?q?elMapper=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/tree/service/TreeServiceImpl.java | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index 56b163c..f02afb4 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -1,10 +1,7 @@ package com.chukapoka.server.tree.service; import com.chukapoka.server.common.dto.CustomUser; -import com.chukapoka.server.tree.dto.TreeList; -import com.chukapoka.server.tree.dto.TreeListResponseDto; -import com.chukapoka.server.tree.dto.TreeRequestDto; -import com.chukapoka.server.tree.dto.TreeModifyRequestDto; +import com.chukapoka.server.tree.dto.*; import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.repository.TreeRepository; import jakarta.persistence.EntityNotFoundException; @@ -15,7 +12,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.List; @Service @@ -28,11 +24,14 @@ public class TreeServiceImpl implements TreeService{ /** 트리생성 */ @Override @Transactional - public Tree createTree(TreeRequestDto treeRequestDto) { + public Tree createTree(TreeCreateRequestDto treeRequestDto) { Tree tree = new Tree(); // 클라이언트에서 입력 받을 필요없이 토큰으로 접속후 권한id로 셋팅 long userId = ((CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); treeRequestDto.setUpdatedBy(userId); + + // 질문 : 여기를 build를 사용하는게 좋을지 modelMapper를 사용하는게 좋을지?? + // -> 서버에서 linkId, sendId를 만들어줄꺼라면 build가 좋을꺼같은데... BeanUtils.copyProperties(treeRequestDto, tree); // mapper대신 사용할수 있지만 복사할 속성의 수가 적고 속성 이름이 일치하는 경우에 적합 return treeRepository.save(tree); } @@ -41,43 +40,38 @@ public Tree createTree(TreeRequestDto treeRequestDto) { @Override public TreeListResponseDto treeList() { List trees = treeRepository.findAllTrees(); - if (trees.isEmpty()) { - return new TreeListResponseDto(new ArrayList<>()); - } return new TreeListResponseDto(trees); } /** 트리 상세 정보 조회 (상세정보 모델) */ @Override - public Tree treeDetail(Long treeId) { - - return null; + public TreeDetailResponseDto treeDetail(Long treeId) { + Tree tree = findTreeByIdOrThrow(treeId); + // tree entity를 TreeDetailResponseDto로 매핑 + return modelMapper.map(tree, TreeDetailResponseDto.class); } /** 트리수정 */ @Override - @Transactional // 데이터 변경감지 public Tree treeModify(Long treeId, TreeModifyRequestDto treeModifyDto) { // 트리 아이디로 트리를 찾음 - Tree tree = treeRepository.findById(treeId) - .orElseThrow(() -> new EntityNotFoundException("treeId " + treeId + " 를 찾을 수 없습니다.")); - updateTreeAttributes(tree, treeModifyDto); - return treeRepository.save(tree); - + Tree tree = findTreeByIdOrThrow(treeId); + return treeUpdate(tree, treeModifyDto); } /** 트리 삭제 */ @Override @Transactional - public void deleteTree(Long treeId) { - treeRepository.findById(treeId) - .orElseThrow(() -> new EntityNotFoundException("등록되지 않은 " + treeId + "입니다.")); + public void treeDelete(Long treeId) { + findTreeByIdOrThrow(treeId); treeRepository.deleteById(treeId); } - /** 트리필드 정보 업데이트 */ - private void updateTreeAttributes(Tree tree, TreeModifyRequestDto treeModifyDto) { + + /** 트리필드 정보 업데이트 메서드*/ + @Transactional // 데이터 변경감지 + private Tree treeUpdate(Tree tree, TreeModifyRequestDto treeModifyDto) { tree.setTitle(treeModifyDto.getTitle()); tree.setType(treeModifyDto.getType()); tree.setTreeBgColor(treeModifyDto.getTreeBgColor()); @@ -85,5 +79,12 @@ private void updateTreeAttributes(Tree tree, TreeModifyRequestDto treeModifyDto) tree.setTreeTopColor(treeModifyDto.getTreeTopColor()); tree.setTreeItemColor(treeModifyDto.getTreeItemColor()); tree.setTreeBottomColor(treeModifyDto.getTreeBottomColor()); + return treeRepository.save(tree); + } + + /** treeId Exception 처리 메서드 */ + private Tree findTreeByIdOrThrow(Long treeId) { + return treeRepository.findById(treeId) + .orElseThrow(() -> new EntityNotFoundException("등록되지 않은 " + treeId + "입니다.")); } } From a6e56ce28fec818123c5625b6a175d55f1bb73cd Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Tue, 27 Feb 2024 23:45:07 +0900 Subject: [PATCH 08/25] TreeItem Test --- .../tree/controller/TreeController.java | 2 + .../server/tree/service/TreeServiceImpl.java | 1 + .../server/treeItem/entity/TreeItem.java | 71 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java diff --git a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java index e5c9ccb..d7431f2 100644 --- a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java +++ b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java @@ -60,5 +60,7 @@ public BaseResponse treeDelete(@PathVariable("treeId") Long treeId) { return new BaseResponse<>(ResultType.SUCCESS, null); } + + } diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index f02afb4..c937318 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -34,6 +34,7 @@ public Tree createTree(TreeCreateRequestDto treeRequestDto) { // -> 서버에서 linkId, sendId를 만들어줄꺼라면 build가 좋을꺼같은데... BeanUtils.copyProperties(treeRequestDto, tree); // mapper대신 사용할수 있지만 복사할 속성의 수가 적고 속성 이름이 일치하는 경우에 적합 return treeRepository.save(tree); + } /** 사용자 트리 리스트 조회(리스트용 모델) */ diff --git a/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java b/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java new file mode 100644 index 0000000..e8d3683 --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java @@ -0,0 +1,71 @@ +package com.chukapoka.server.treeItem.entity; + + +import com.chukapoka.server.tree.entity.Tree; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicUpdate; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDateTime; + +@Entity +@Data +@AllArgsConstructor +@NoArgsConstructor +@DynamicUpdate // 데이터의 변경사항이 있는 것만 수정 +@Table(name = "tb_treeItem") +public class TreeItem { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "treeItemId") + private Long treeItemId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "treeId", referencedColumnName = "treeId") + private Tree treeId; + + /** 편지 제목 */ + @Column(name = "title") + private String title; + + /** 편지 내용*/ + @Column(name = "content") + private String content; + + /** 트라 관련 색상은 String -> enum type으로 상수로 바꿔야 관리가 더 편할것같음 */ + @Column(name = "treeBgColor", nullable = true) + private String treeBgColor; + + @Column(name = "groundColor", nullable = true) + private String groundColor; + + @Column(name = "treeTopColor", nullable = true) + private String treeTopColor; + + @Column(name = "treeItemColor", nullable = true) + private String treeItemColor; + + @Column(name = "treeBottomColor", nullable = true) + private String treeBottomColor; + + /** userId가 값임 */ + @Column(name = "updatedBy") + private Long updatedBy; + + /** 생성 시간 */ + @Column(name = "updatedAt", nullable = false) + @LastModifiedDate + private LocalDateTime updatedAt; + + + + @PrePersist + public void updatedAt() { + this.updatedAt = LocalDateTime.now(); + } + + +} From b89688b834b94dd69d53bdd0985f2c3e7a06521a Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Wed, 28 Feb 2024 23:55:00 +0900 Subject: [PATCH 09/25] =?UTF-8?q?TreeItem=20=EC=83=9D=EC=84=B1=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/authority/SecurityConfig.java | 2 +- .../tree/controller/TreeController.java | 4 +-- .../server/tree/service/TreeService.java | 2 +- .../server/tree/service/TreeServiceImpl.java | 8 +++-- .../controller/TreeItemController.java | 24 +++++++++++++ .../server/treeItem/entity/TreeItem.java | 35 ++++++------------- .../repository/TreeItemRepository.java | 8 +++++ .../treeItem/service/TreeItemService.java | 12 +++++++ .../treeItem/service/TreeItemServiceImpl.java | 28 +++++++++++++++ 9 files changed, 92 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java create mode 100644 src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java create mode 100644 src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java create mode 100644 src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java diff --git a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java index bb3d47e..b7694b0 100644 --- a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java +++ b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java @@ -37,7 +37,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { authorizeRequests .requestMatchers("/api/user/emailCheck", "/api/user", "/api/user/authNumber", "/api/health").anonymous() - .requestMatchers("/api/user/logout", "api/user/reissue","/api/tree","api/tree/**").hasRole(Authority.USER.getAuthority());// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 + .requestMatchers("/api/user/logout", "api/user/reissue","/api/tree","api/tree/**","api/treeItem").hasRole(Authority.USER.getAuthority());// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 } diff --git a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java index d7431f2..e81203e 100644 --- a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java +++ b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java @@ -25,8 +25,8 @@ public class TreeController { /**트리 생성 */ @PostMapping - public BaseResponsecreateTree(@Valid @RequestBody TreeCreateRequestDto treeRequestDto) { - Tree responseDto = treeService.createTree(treeRequestDto); + public BaseResponsecreateTree(@Valid @RequestBody TreeCreateRequestDto treeRequestDto) { + Long responseDto = treeService.createTree(treeRequestDto); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeService.java b/src/main/java/com/chukapoka/server/tree/service/TreeService.java index 61b81af..95070e2 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeService.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeService.java @@ -9,7 +9,7 @@ public interface TreeService { /** 트리 저장 */ - Tree createTree(TreeCreateRequestDto treeRequestDto); + Long createTree(TreeCreateRequestDto treeRequestDto); /** 트리리스트 조회(리스트용 모델) */ TreeListResponseDto treeList(); diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index c937318..f21adc1 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -24,7 +24,7 @@ public class TreeServiceImpl implements TreeService{ /** 트리생성 */ @Override @Transactional - public Tree createTree(TreeCreateRequestDto treeRequestDto) { + public Long createTree(TreeCreateRequestDto treeRequestDto) { Tree tree = new Tree(); // 클라이언트에서 입력 받을 필요없이 토큰으로 접속후 권한id로 셋팅 long userId = ((CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); @@ -33,8 +33,10 @@ public Tree createTree(TreeCreateRequestDto treeRequestDto) { // 질문 : 여기를 build를 사용하는게 좋을지 modelMapper를 사용하는게 좋을지?? // -> 서버에서 linkId, sendId를 만들어줄꺼라면 build가 좋을꺼같은데... BeanUtils.copyProperties(treeRequestDto, tree); // mapper대신 사용할수 있지만 복사할 속성의 수가 적고 속성 이름이 일치하는 경우에 적합 - return treeRepository.save(tree); - + Tree saveTree = treeRepository.save(tree); + + return saveTree.getTreeId(); + } /** 사용자 트리 리스트 조회(리스트용 모델) */ diff --git a/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java b/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java new file mode 100644 index 0000000..4065aad --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java @@ -0,0 +1,24 @@ +package com.chukapoka.server.treeItem.controller; + +import com.chukapoka.server.common.dto.BaseResponse; +import com.chukapoka.server.common.enums.ResultType; +import com.chukapoka.server.treeItem.entity.TreeItem; +import com.chukapoka.server.treeItem.service.TreeItemService; +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +@RequestMapping("api/treeItem") +public class TreeItemController { + + private final TreeItemService treeItemService; + @PostMapping + public BaseResponse createTreeItem(@RequestParam("treeId") Long treeId) { + TreeItem responseDto = treeItemService.createTreeItem(treeId); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); + } +} diff --git a/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java b/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java index e8d3683..ca41858 100644 --- a/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java +++ b/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java @@ -10,6 +10,7 @@ import org.springframework.data.annotation.LastModifiedDate; import java.time.LocalDateTime; +import java.util.concurrent.atomic.AtomicInteger; @Entity @Data @@ -19,13 +20,12 @@ @Table(name = "tb_treeItem") public class TreeItem { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "treeItemId") - private Long treeItemId; + @Column(name = "treeItemId", unique = true, nullable = false) + private String treeItemId; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "treeId", referencedColumnName = "treeId") - private Tree treeId; + private Long treeId; /** 편지 제목 */ @Column(name = "title") @@ -35,22 +35,6 @@ public class TreeItem { @Column(name = "content") private String content; - /** 트라 관련 색상은 String -> enum type으로 상수로 바꿔야 관리가 더 편할것같음 */ - @Column(name = "treeBgColor", nullable = true) - private String treeBgColor; - - @Column(name = "groundColor", nullable = true) - private String groundColor; - - @Column(name = "treeTopColor", nullable = true) - private String treeTopColor; - - @Column(name = "treeItemColor", nullable = true) - private String treeItemColor; - - @Column(name = "treeBottomColor", nullable = true) - private String treeBottomColor; - /** userId가 값임 */ @Column(name = "updatedBy") private Long updatedBy; @@ -60,12 +44,15 @@ public class TreeItem { @LastModifiedDate private LocalDateTime updatedAt; - - - @PrePersist - public void updatedAt() { + @PrePersist // JPA에서는 엔티티의 생명주기 중 하나의 이벤트에 대해 하나의 @PrePersist 메서드만을 허용 + public void prePersist() { + this.treeItemId = TreeItemId(); this.updatedAt = LocalDateTime.now(); } + private static final AtomicInteger counter = new AtomicInteger(0); + private static String TreeItemId() { + return "treeItem" + counter.incrementAndGet(); + } } diff --git a/src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java b/src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java new file mode 100644 index 0000000..a894225 --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java @@ -0,0 +1,8 @@ +package com.chukapoka.server.treeItem.repository; + +import com.chukapoka.server.treeItem.entity.TreeItem; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TreeItemRepository extends JpaRepository { + +} diff --git a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java new file mode 100644 index 0000000..e0de53d --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java @@ -0,0 +1,12 @@ +package com.chukapoka.server.treeItem.service; + + +import com.chukapoka.server.treeItem.entity.TreeItem; +import org.springframework.stereotype.Service; + + +public interface TreeItemService { + + TreeItem createTreeItem(Long treeId); + +} diff --git a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java new file mode 100644 index 0000000..6763432 --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java @@ -0,0 +1,28 @@ +package com.chukapoka.server.treeItem.service; + +import com.chukapoka.server.tree.entity.Tree; +import com.chukapoka.server.tree.repository.TreeRepository; +import com.chukapoka.server.tree.service.TreeService; +import com.chukapoka.server.treeItem.entity.TreeItem; +import com.chukapoka.server.treeItem.repository.TreeItemRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class TreeItemServiceImpl implements TreeItemService{ + + private final TreeItemRepository treeItemRepository; + private final TreeRepository treeRepository; + @Override + public TreeItem createTreeItem(Long treeId) { + Tree tree = treeRepository.findById(treeId) + .orElseThrow(() -> new EntityNotFoundException("Tree not found with id: " + treeId)); + + TreeItem treeItem = new TreeItem(); + treeItem.setTreeId(treeId); + + return treeItemRepository.save(treeItem); + } +} From 60a7aabef187c86e7ad3b10d27c33deb62c7ed6e Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Thu, 29 Feb 2024 20:58:11 +0900 Subject: [PATCH 10/25] =?UTF-8?q?AppConfig=20-=20moddelMapper=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20con?= =?UTF-8?q?fig=EC=84=A4=EC=A0=95=20Tree,=20TreeItem=20entity=20-=20id?= =?UTF-8?q?=EA=B0=92=EC=9D=84=20String=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20service,=20controller?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20Tree=20=20=20Controller=20=20=20-=20cre?= =?UTF-8?q?ateTree,=20treeDetail,=20treeModify=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=EA=B0=92=EC=9D=84=20TreeDetailResponseDto=20=EB=A1=9C=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=20=20-=20treeDetail=EB=A1=9C=20=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=EC=A0=95=EB=B3=B4=20=EA=B0=80=EC=A0=B8?= =?UTF-8?q?=EC=98=AC=20=EB=95=8C=20=ED=8A=B8=EB=A6=AC=EC=95=84=EC=9D=B4?= =?UTF-8?q?=ED=85=9C=20=EB=AA=A9=EB=A1=9D=20=EA=B0=80=EC=A0=B8=EC=98=AC?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EB=A7=8C=EB=93=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TreeItem - treeItem 생성메서드 만듬 --- .../server/common/authority/AppConfig.java | 11 +++- .../tree/controller/TreeController.java | 14 ++--- .../server/tree/dto/TreeCreateRequestDto.java | 2 +- .../tree/dto/TreeDetailResponseDto.java | 16 +++-- .../chukapoka/server/tree/dto/TreeList.java | 10 ++-- .../server/tree/dto/TreeModifyRequestDto.java | 5 ++ .../chukapoka/server/tree/entity/Tree.java | 18 +++--- .../tree/repository/TreeRepository.java | 5 +- .../server/tree/service/TreeService.java | 9 ++- .../server/tree/service/TreeServiceImpl.java | 58 +++++++++---------- .../controller/TreeItemController.java | 13 +++-- .../dto/TreeItemCreateRequestDto.java | 25 ++++++++ .../dto/TreeItemDetailResponseDto.java | 23 ++++++++ .../treeItem/dto/TreeItemListResponseDto.java | 14 +++++ .../server/treeItem/entity/TreeItem.java | 16 ++--- .../repository/TreeItemRepository.java | 4 +- .../treeItem/service/TreeItemService.java | 10 ++-- .../treeItem/service/TreeItemServiceImpl.java | 35 +++++++++-- 18 files changed, 196 insertions(+), 92 deletions(-) create mode 100644 src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java create mode 100644 src/main/java/com/chukapoka/server/treeItem/dto/TreeItemDetailResponseDto.java create mode 100644 src/main/java/com/chukapoka/server/treeItem/dto/TreeItemListResponseDto.java diff --git a/src/main/java/com/chukapoka/server/common/authority/AppConfig.java b/src/main/java/com/chukapoka/server/common/authority/AppConfig.java index cc95cde..dcfbdd1 100644 --- a/src/main/java/com/chukapoka/server/common/authority/AppConfig.java +++ b/src/main/java/com/chukapoka/server/common/authority/AppConfig.java @@ -1,15 +1,22 @@ package com.chukapoka.server.common.authority; +import lombok.RequiredArgsConstructor; import org.modelmapper.ModelMapper; +import org.modelmapper.convention.MatchingStrategies; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration +@RequiredArgsConstructor public class AppConfig { - @Bean public ModelMapper modelMapper() { - return new ModelMapper(); + ModelMapper modelMapper = new ModelMapper(); + /** 연결 전략 : 같은 타입의 필드명이 같은 경우만 동작 */ + modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.LOOSE).setSkipNullEnabled(true).setFieldMatchingEnabled(true) + .setAmbiguityIgnored(true) // id속성을 매핑에서 제외 + .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE);; + return modelMapper; } } diff --git a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java index e81203e..33b4a28 100644 --- a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java +++ b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java @@ -25,8 +25,8 @@ public class TreeController { /**트리 생성 */ @PostMapping - public BaseResponsecreateTree(@Valid @RequestBody TreeCreateRequestDto treeRequestDto) { - Long responseDto = treeService.createTree(treeRequestDto); + public BaseResponsecreateTree(@Valid @RequestBody TreeCreateRequestDto treeRequestDto) { + TreeDetailResponseDto responseDto = treeService.createTree(treeRequestDto); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } @@ -39,23 +39,23 @@ public BaseResponse treeList() { /** 트리상세 정보 */ @GetMapping("/{treeId}") - private BaseResponse treeDetail(@PathVariable("treeId") Long treeId) { + private BaseResponse treeDetail(@PathVariable("treeId") String treeId) { TreeDetailResponseDto responseDto = treeService.treeDetail(treeId); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } /** 트리 수정 */ @PutMapping("/{treeId}") - public BaseResponse treeModify(@PathVariable("treeId") Long treeId, - @RequestBody TreeModifyRequestDto treeModifyDto) { - Tree responseDto = treeService.treeModify(treeId, treeModifyDto); + public BaseResponse treeModify(@PathVariable("treeId") String treeId, + @Valid @RequestBody TreeModifyRequestDto treeModifyDto) { + TreeDetailResponseDto responseDto = treeService.treeModify(treeId, treeModifyDto); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } /** 트리 삭제 */ @DeleteMapping("/{treeId}") - public BaseResponse treeDelete(@PathVariable("treeId") Long treeId) { + public BaseResponse treeDelete(@PathVariable("treeId") String treeId) { treeService.treeDelete(treeId); return new BaseResponse<>(ResultType.SUCCESS, null); } diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeCreateRequestDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeCreateRequestDto.java index cbf0c9d..ec2956f 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeCreateRequestDto.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeCreateRequestDto.java @@ -10,7 +10,7 @@ public class TreeCreateRequestDto { @NotBlank(message = "title is null") private String title; - @NotNull(message = "treeType is null") + @NotBlank(message = "treeType is null") @ValidEnum(enumClass = TreeType.class, message = "TreeType must be MINE or NOT_YET_SEND") private String type; @NotBlank(message = "linkId is null") diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java index 37f0c64..f01bb83 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java @@ -1,33 +1,37 @@ package com.chukapoka.server.tree.dto; - - +import com.chukapoka.server.tree.entity.Tree; +import com.chukapoka.server.treeItem.entity.TreeItem; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import java.util.List; @Data @AllArgsConstructor @NoArgsConstructor +@Builder public class TreeDetailResponseDto { /** 트리상세 정보 */ - private Long treeId; + private String treeId; private String title; private String type; // MINE or NOT_YEN_SEND + private String linkId; + private String sendId; private String treeBgColor; private String groundColor; private String treeTopColor; private String treeItemColor; private String treeBottomColor; - - /** treeItem 목록 필요 */ - private Long updatedBy; private LocalDateTime updatedAt; + /** treeItem 목록 */ + private List treeItem; } diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeList.java b/src/main/java/com/chukapoka/server/tree/dto/TreeList.java index a31090e..5847a75 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeList.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeList.java @@ -7,19 +7,21 @@ import java.time.LocalDateTime; @Data -@AllArgsConstructor -@NoArgsConstructor public class TreeList { /** 트리 리스트정보 */ - private Long treeId; + private String treeId; private String title; private String type; // MINE or NOT_YEN_SEND private String linkId; private String sendId; private Long updatedBy; private LocalDateTime updatedAt; - /** 트리색상 부분 추가 가능 */ + private String treeBgColor; + private String groundColor; + private String treeTopColor; + private String treeItemColor; + private String treeBottomColor; } diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java index 4db3135..252537f 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java @@ -1,5 +1,8 @@ package com.chukapoka.server.tree.dto; +import com.chukapoka.server.common.annotation.ValidEnum; +import com.chukapoka.server.common.enums.TreeType; +import jakarta.validation.constraints.NotBlank; import lombok.Data; @Data @@ -7,6 +10,8 @@ public class TreeModifyRequestDto { /** 트리에 관련된 것만 수정할것인지 ?*/ private String title; + @NotBlank(message = "treeType is null") + @ValidEnum(enumClass = TreeType.class, message = "TreeType must be MINE or NOT_YET_SEND") private String type; private String treeBgColor; private String groundColor; diff --git a/src/main/java/com/chukapoka/server/tree/entity/Tree.java b/src/main/java/com/chukapoka/server/tree/entity/Tree.java index fc56ae9..16e3422 100644 --- a/src/main/java/com/chukapoka/server/tree/entity/Tree.java +++ b/src/main/java/com/chukapoka/server/tree/entity/Tree.java @@ -1,27 +1,28 @@ package com.chukapoka.server.tree.entity; - import jakarta.persistence.*; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.DynamicUpdate; import org.springframework.data.annotation.LastModifiedDate; import java.time.LocalDateTime; +import java.util.concurrent.atomic.AtomicInteger; @Entity @Data @AllArgsConstructor @NoArgsConstructor @DynamicUpdate // 데이터의 변경사항이 있는 것만 수정 +@Builder @Table(name = "tb_tree") public class Tree { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "treeId") - private Long treeId; + @Column(name = "treeId", unique = true, nullable = false) + private String treeId; /** 트리제목 */ @Column(name = "title") @@ -64,12 +65,15 @@ public class Tree { @LastModifiedDate private LocalDateTime updatedAt; - - @PrePersist - public void updatedAt() { + public void prePersist() { this.updatedAt = LocalDateTime.now(); + this.treeId = TreeId(); } + private static final AtomicInteger counter = new AtomicInteger(0); + private static String TreeId() { + return "treeId" + counter.incrementAndGet(); + } } diff --git a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java index b381de8..64beeff 100644 --- a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java +++ b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java @@ -13,10 +13,11 @@ import java.util.Optional; @Repository -public interface TreeRepository extends JpaRepository { - Optional findById(Long treeId); +public interface TreeRepository extends JpaRepository { + Optional findById(String treeId); @Query("SELECT new com.chukapoka.server.tree.dto.TreeList(tree.treeId, tree.title, tree.type, tree.linkId, tree.sendId, tree.updatedBy, tree.updatedAt) FROM Tree tree") List findAllTrees(); + } diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeService.java b/src/main/java/com/chukapoka/server/tree/service/TreeService.java index 95070e2..fe6cbd5 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeService.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeService.java @@ -4,23 +4,22 @@ import com.chukapoka.server.tree.dto.TreeListResponseDto; import com.chukapoka.server.tree.dto.TreeCreateRequestDto; import com.chukapoka.server.tree.dto.TreeModifyRequestDto; -import com.chukapoka.server.tree.entity.Tree; public interface TreeService { /** 트리 저장 */ - Long createTree(TreeCreateRequestDto treeRequestDto); + TreeDetailResponseDto createTree(TreeCreateRequestDto treeRequestDto); /** 트리리스트 조회(리스트용 모델) */ TreeListResponseDto treeList(); /** 트리 상세 정보 조회 (상세정보 모델) */ - TreeDetailResponseDto treeDetail(Long treeId); + TreeDetailResponseDto treeDetail(String treeId); /** 트리 수정 */ - Tree treeModify(Long treeId, TreeModifyRequestDto treeModifyDto); + TreeDetailResponseDto treeModify(String treeId, TreeModifyRequestDto treeModifyDto); /** 트리 삭제 */ - void treeDelete(Long treeId); + void treeDelete(String treeId); } diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index f21adc1..2af2515 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -4,39 +4,39 @@ import com.chukapoka.server.tree.dto.*; import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.repository.TreeRepository; +import com.chukapoka.server.treeItem.entity.TreeItem; +import com.chukapoka.server.treeItem.repository.TreeItemRepository; import jakarta.persistence.EntityNotFoundException; import lombok.AllArgsConstructor; import org.modelmapper.ModelMapper; -import org.springframework.beans.BeanUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; + @Service @AllArgsConstructor public class TreeServiceImpl implements TreeService{ private final TreeRepository treeRepository; + private final TreeItemRepository treeItemRepository; private final ModelMapper modelMapper; /** 트리생성 */ @Override @Transactional - public Long createTree(TreeCreateRequestDto treeRequestDto) { - Tree tree = new Tree(); + public TreeDetailResponseDto createTree(TreeCreateRequestDto treeRequestDto) { // 클라이언트에서 입력 받을 필요없이 토큰으로 접속후 권한id로 셋팅 long userId = ((CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + Tree tree = new Tree(); treeRequestDto.setUpdatedBy(userId); - // 질문 : 여기를 build를 사용하는게 좋을지 modelMapper를 사용하는게 좋을지?? - // -> 서버에서 linkId, sendId를 만들어줄꺼라면 build가 좋을꺼같은데... - BeanUtils.copyProperties(treeRequestDto, tree); // mapper대신 사용할수 있지만 복사할 속성의 수가 적고 속성 이름이 일치하는 경우에 적합 - Tree saveTree = treeRepository.save(tree); - - return saveTree.getTreeId(); - +// BeanUtils.copyProperties(treeRequestDto, tree); // mapper대신 사용할수 있지만 복사할 속성의 수가 적고 속성 이름이 일치하는 경우에 적합 + modelMapper.map(treeRequestDto, tree); + treeRepository.save(tree); + return modelMapper.map(tree, TreeDetailResponseDto.class); } /** 사용자 트리 리스트 조회(리스트용 모델) */ @@ -48,45 +48,39 @@ public TreeListResponseDto treeList() { /** 트리 상세 정보 조회 (상세정보 모델) */ @Override - public TreeDetailResponseDto treeDetail(Long treeId) { + public TreeDetailResponseDto treeDetail(String treeId) { Tree tree = findTreeByIdOrThrow(treeId); - // tree entity를 TreeDetailResponseDto로 매핑 - return modelMapper.map(tree, TreeDetailResponseDto.class); + // 트리에 속한 모든 TreeItem을 가져오기 + List treeItems = treeItemRepository.findByTreeId(tree.getTreeId()); + TreeDetailResponseDto treeDetailResponseDto = modelMapper.map(tree, TreeDetailResponseDto.class); + treeDetailResponseDto.setTreeItem(treeItems); + return treeDetailResponseDto; + } /** 트리수정 */ @Override - public Tree treeModify(Long treeId, TreeModifyRequestDto treeModifyDto) { + @Transactional + public TreeDetailResponseDto treeModify(String treeId, TreeModifyRequestDto treeModifyDto) { // 트리 아이디로 트리를 찾음 Tree tree = findTreeByIdOrThrow(treeId); - return treeUpdate(tree, treeModifyDto); + modelMapper.map(treeModifyDto, tree); + // 변경된 트리 저장 + treeRepository.save(tree); + // 변경된 트리 상세 정보 반환 + return modelMapper.map(tree, TreeDetailResponseDto.class); } /** 트리 삭제 */ @Override @Transactional - public void treeDelete(Long treeId) { + public void treeDelete(String treeId) { findTreeByIdOrThrow(treeId); treeRepository.deleteById(treeId); } - - - /** 트리필드 정보 업데이트 메서드*/ - @Transactional // 데이터 변경감지 - private Tree treeUpdate(Tree tree, TreeModifyRequestDto treeModifyDto) { - tree.setTitle(treeModifyDto.getTitle()); - tree.setType(treeModifyDto.getType()); - tree.setTreeBgColor(treeModifyDto.getTreeBgColor()); - tree.setGroundColor(treeModifyDto.getGroundColor()); - tree.setTreeTopColor(treeModifyDto.getTreeTopColor()); - tree.setTreeItemColor(treeModifyDto.getTreeItemColor()); - tree.setTreeBottomColor(treeModifyDto.getTreeBottomColor()); - return treeRepository.save(tree); - } - /** treeId Exception 처리 메서드 */ - private Tree findTreeByIdOrThrow(Long treeId) { + private Tree findTreeByIdOrThrow(String treeId) { return treeRepository.findById(treeId) .orElseThrow(() -> new EntityNotFoundException("등록되지 않은 " + treeId + "입니다.")); } diff --git a/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java b/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java index 4065aad..4de33c1 100644 --- a/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java +++ b/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java @@ -2,13 +2,13 @@ import com.chukapoka.server.common.dto.BaseResponse; import com.chukapoka.server.common.enums.ResultType; +import com.chukapoka.server.treeItem.dto.TreeItemCreateRequestDto; +import com.chukapoka.server.treeItem.dto.TreeItemDetailResponseDto; import com.chukapoka.server.treeItem.entity.TreeItem; import com.chukapoka.server.treeItem.service.TreeItemService; +import jakarta.validation.Valid; import lombok.AllArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @AllArgsConstructor @@ -16,9 +16,10 @@ public class TreeItemController { private final TreeItemService treeItemService; + /** 트리아이템 생성 */ @PostMapping - public BaseResponse createTreeItem(@RequestParam("treeId") Long treeId) { - TreeItem responseDto = treeItemService.createTreeItem(treeId); + public BaseResponse createTreeItem(@Valid @RequestBody TreeItemCreateRequestDto treeItemCreateRequestDto) { + TreeItemDetailResponseDto responseDto = treeItemService.createTreeItem(treeItemCreateRequestDto); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } } diff --git a/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java new file mode 100644 index 0000000..6e9d5db --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java @@ -0,0 +1,25 @@ +package com.chukapoka.server.treeItem.dto; + +import com.chukapoka.server.tree.entity.Tree; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class TreeItemCreateRequestDto { + + @NotNull(message = "treeId is null") + private String treeId; + @NotBlank(message = "title is null") + private String title; + @NotBlank(message = "content is null") + private String content; + @NotBlank(message = "treeItemColor is null") + private String treeItemColor; + private Long updatedBy; + private LocalDateTime updatedAt; +} + + diff --git a/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemDetailResponseDto.java b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemDetailResponseDto.java new file mode 100644 index 0000000..3d36551 --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemDetailResponseDto.java @@ -0,0 +1,23 @@ +package com.chukapoka.server.treeItem.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TreeItemDetailResponseDto { + + /** 트리 아이템 상세정보 */ + private String id; + private String treeId; + private String title; + private String content; + private String treeItemColor; + private Long updatedBy; + private LocalDateTime updatedAt; + +} diff --git a/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemListResponseDto.java b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemListResponseDto.java new file mode 100644 index 0000000..5f5068f --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemListResponseDto.java @@ -0,0 +1,14 @@ +package com.chukapoka.server.treeItem.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TreeItemListResponseDto { + private List treeItem; +} diff --git a/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java b/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java index ca41858..86bc9ee 100644 --- a/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java +++ b/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java @@ -1,7 +1,6 @@ package com.chukapoka.server.treeItem.entity; -import com.chukapoka.server.tree.entity.Tree; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Data; @@ -21,11 +20,10 @@ public class TreeItem { @Id @Column(name = "treeItemId", unique = true, nullable = false) - private String treeItemId; + private String id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "treeId", referencedColumnName = "treeId") - private Long treeId; + @Column(name = "treeId") + private String treeId; /** 편지 제목 */ @Column(name = "title") @@ -35,6 +33,10 @@ public class TreeItem { @Column(name = "content") private String content; + /** 트리아이템 색상 */ + @Column(name = "treeItemColor", nullable = true) + private String treeItemColor; + /** userId가 값임 */ @Column(name = "updatedBy") private Long updatedBy; @@ -46,10 +48,8 @@ public class TreeItem { @PrePersist // JPA에서는 엔티티의 생명주기 중 하나의 이벤트에 대해 하나의 @PrePersist 메서드만을 허용 public void prePersist() { - this.treeItemId = TreeItemId(); - this.updatedAt = LocalDateTime.now(); + this.id = TreeItemId(); } - private static final AtomicInteger counter = new AtomicInteger(0); private static String TreeItemId() { return "treeItem" + counter.incrementAndGet(); diff --git a/src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java b/src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java index a894225..652b776 100644 --- a/src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java +++ b/src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java @@ -3,6 +3,8 @@ import com.chukapoka.server.treeItem.entity.TreeItem; import org.springframework.data.jpa.repository.JpaRepository; -public interface TreeItemRepository extends JpaRepository { +import java.util.List; +public interface TreeItemRepository extends JpaRepository { + List findByTreeId(String treeId); } diff --git a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java index e0de53d..1c8292e 100644 --- a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java +++ b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java @@ -1,12 +1,10 @@ package com.chukapoka.server.treeItem.service; - - -import com.chukapoka.server.treeItem.entity.TreeItem; -import org.springframework.stereotype.Service; - +import com.chukapoka.server.treeItem.dto.TreeItemCreateRequestDto; +import com.chukapoka.server.treeItem.dto.TreeItemDetailResponseDto; public interface TreeItemService { - TreeItem createTreeItem(Long treeId); + /** 트리 아이템 생성 */ + TreeItemDetailResponseDto createTreeItem(TreeItemCreateRequestDto treeItemCreateRequestDto); } diff --git a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java index 6763432..cebbc88 100644 --- a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java +++ b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java @@ -1,13 +1,20 @@ package com.chukapoka.server.treeItem.service; +import com.chukapoka.server.common.dto.CustomUser; import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.repository.TreeRepository; -import com.chukapoka.server.tree.service.TreeService; +import com.chukapoka.server.treeItem.dto.TreeItemCreateRequestDto; +import com.chukapoka.server.treeItem.dto.TreeItemDetailResponseDto; import com.chukapoka.server.treeItem.entity.TreeItem; import com.chukapoka.server.treeItem.repository.TreeItemRepository; import jakarta.persistence.EntityNotFoundException; import lombok.AllArgsConstructor; +import org.modelmapper.ModelMapper; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; @Service @AllArgsConstructor @@ -15,14 +22,32 @@ public class TreeItemServiceImpl implements TreeItemService{ private final TreeItemRepository treeItemRepository; private final TreeRepository treeRepository; + private final ModelMapper modelMapper; + + /** 트리 아이템 생성 */ @Override - public TreeItem createTreeItem(Long treeId) { + @Transactional + public TreeItemDetailResponseDto createTreeItem(TreeItemCreateRequestDto treeItemCreateRequestDto) { + String treeId = treeItemCreateRequestDto.getTreeId(); + long userId = ((CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + return saveTreeItem(treeId, userId, treeItemCreateRequestDto); + } + + /** 트라이이템 저장 메서드 */ + private TreeItemDetailResponseDto saveTreeItem(String treeId, long userId, TreeItemCreateRequestDto treeItemCreateRequestDto) { + // 트리 객체 조회 Tree tree = treeRepository.findById(treeId) .orElseThrow(() -> new EntityNotFoundException("Tree not found with id: " + treeId)); - - TreeItem treeItem = new TreeItem(); + // 트리 아이템 생성 및 저장 + TreeItem treeItem = modelMapper.map(treeItemCreateRequestDto, TreeItem.class); treeItem.setTreeId(treeId); + treeItem.setUpdatedBy(userId); + treeItem.setUpdatedAt(LocalDateTime.now()); + treeItemRepository.save(treeItem); - return treeItemRepository.save(treeItem); + return modelMapper.map(treeItem, TreeItemDetailResponseDto.class); } } + + + From 25b9f363d832a946e014a9114deb753f1e8f1b7b Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Thu, 29 Feb 2024 23:17:55 +0900 Subject: [PATCH 11/25] =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=ED=8A=B8=EB=A6=AC=EC=95=84=EC=9D=B4?= =?UTF-8?q?=ED=85=9C(=EB=A6=AC=EC=8A=A4=ED=8A=B8)=20Get=20"api/treeItem"?= =?UTF-8?q?=20=ED=8A=B8=EB=9D=BC=EC=95=84=EC=9D=B4=ED=85=9C(=EC=83=81?= =?UTF-8?q?=EC=84=B8=EC=A0=95=EB=B3=B4)=20Get=20"api/treeItem/{treeItemId}?= =?UTF-8?q?"=20=ED=8A=B8=EB=A6=AC=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20PUT=20"api/treeItem/{treeItemId}"=20=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=82=AD=EC=A0=9C=20DEL?= =?UTF-8?q?ETE=20"api/treeItem/{treeItemId}"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/common/authority/AppConfig.java | 2 +- .../common/authority/SecurityConfig.java | 2 +- .../server/tree/service/TreeServiceImpl.java | 2 + .../controller/TreeItemController.java | 33 ++++++++++- .../dto/TreeItemModifyRequestDto.java | 11 ++++ .../treeItem/service/TreeItemService.java | 13 +++++ .../treeItem/service/TreeItemServiceImpl.java | 55 +++++++++++++++++++ 7 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/chukapoka/server/treeItem/dto/TreeItemModifyRequestDto.java diff --git a/src/main/java/com/chukapoka/server/common/authority/AppConfig.java b/src/main/java/com/chukapoka/server/common/authority/AppConfig.java index dcfbdd1..46f442c 100644 --- a/src/main/java/com/chukapoka/server/common/authority/AppConfig.java +++ b/src/main/java/com/chukapoka/server/common/authority/AppConfig.java @@ -16,7 +16,7 @@ public ModelMapper modelMapper() { /** 연결 전략 : 같은 타입의 필드명이 같은 경우만 동작 */ modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.LOOSE).setSkipNullEnabled(true).setFieldMatchingEnabled(true) .setAmbiguityIgnored(true) // id속성을 매핑에서 제외 - .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE);; + .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE); return modelMapper; } } diff --git a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java index b7694b0..ef9fd53 100644 --- a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java +++ b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java @@ -37,7 +37,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { authorizeRequests .requestMatchers("/api/user/emailCheck", "/api/user", "/api/user/authNumber", "/api/health").anonymous() - .requestMatchers("/api/user/logout", "api/user/reissue","/api/tree","api/tree/**","api/treeItem").hasRole(Authority.USER.getAuthority());// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 + .requestMatchers("/api/user/logout", "api/user/reissue","/api/tree","api/tree/**","api/treeItem","api/treeItem/**").hasRole(Authority.USER.getAuthority());// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 } diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index 2af2515..afafd80 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -71,6 +71,8 @@ public TreeDetailResponseDto treeModify(String treeId, TreeModifyRequestDto tree return modelMapper.map(tree, TreeDetailResponseDto.class); } + + /** 트리 삭제 */ @Override @Transactional diff --git a/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java b/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java index 4de33c1..cfbed9c 100644 --- a/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java +++ b/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java @@ -4,7 +4,8 @@ import com.chukapoka.server.common.enums.ResultType; import com.chukapoka.server.treeItem.dto.TreeItemCreateRequestDto; import com.chukapoka.server.treeItem.dto.TreeItemDetailResponseDto; -import com.chukapoka.server.treeItem.entity.TreeItem; +import com.chukapoka.server.treeItem.dto.TreeItemListResponseDto; +import com.chukapoka.server.treeItem.dto.TreeItemModifyRequestDto; import com.chukapoka.server.treeItem.service.TreeItemService; import jakarta.validation.Valid; import lombok.AllArgsConstructor; @@ -22,4 +23,34 @@ public BaseResponse createTreeItem(@Valid @RequestBod TreeItemDetailResponseDto responseDto = treeItemService.createTreeItem(treeItemCreateRequestDto); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } + + /** 트리리스트 목록 */ + @GetMapping + public BaseResponse treeItemList() { + TreeItemListResponseDto responseDto = treeItemService.treeList(); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); + } + + /** 트리상세 정보 */ + @GetMapping("/{treeItemId}") + private BaseResponse treeItemDetail(@PathVariable("treeItemId") String treeItemId) { + TreeItemDetailResponseDto responseDto = treeItemService.treeDetail(treeItemId); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); + } + + /** 트리 수정 */ + @PutMapping("/{treeItemId}") + public BaseResponse treeItemModify(@PathVariable("treeItemId") String treeItemId, + @Valid @RequestBody TreeItemModifyRequestDto treeItemModifyDto) { + TreeItemDetailResponseDto responseDto = treeItemService.treeModify(treeItemId, treeItemModifyDto); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); + } + + + /** 트리 삭제 */ + @DeleteMapping("/{treeItemId}") + public BaseResponse treeItemDelete(@PathVariable("treeItemId") String treeItemId) { + treeItemService.treeItemDelete(treeItemId); + return new BaseResponse<>(ResultType.SUCCESS, null); + } } diff --git a/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemModifyRequestDto.java b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemModifyRequestDto.java new file mode 100644 index 0000000..68e965b --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemModifyRequestDto.java @@ -0,0 +1,11 @@ +package com.chukapoka.server.treeItem.dto; + +import lombok.Data; + +@Data +public class TreeItemModifyRequestDto { + + private String title; + private String content; + private String treeItemColor; +} diff --git a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java index 1c8292e..ac11b4e 100644 --- a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java +++ b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java @@ -1,10 +1,23 @@ package com.chukapoka.server.treeItem.service; import com.chukapoka.server.treeItem.dto.TreeItemCreateRequestDto; import com.chukapoka.server.treeItem.dto.TreeItemDetailResponseDto; +import com.chukapoka.server.treeItem.dto.TreeItemListResponseDto; +import com.chukapoka.server.treeItem.dto.TreeItemModifyRequestDto; public interface TreeItemService { /** 트리 아이템 생성 */ TreeItemDetailResponseDto createTreeItem(TreeItemCreateRequestDto treeItemCreateRequestDto); + /** 트리아이템리스트 조회(리스트용 모델) */ + TreeItemListResponseDto treeList(); + + /** 트리아이템 상세 정보 조회 (상세정보 모델) */ + TreeItemDetailResponseDto treeDetail(String treeItemId); + + /** 트리아이템 수정 */ + TreeItemDetailResponseDto treeModify(String treeItemId, TreeItemModifyRequestDto treeItemModifyDto); + + /** 트리아이템 삭제 */ + void treeItemDelete(String treeItemId); } diff --git a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java index cebbc88..a3d36f2 100644 --- a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java +++ b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java @@ -5,6 +5,8 @@ import com.chukapoka.server.tree.repository.TreeRepository; import com.chukapoka.server.treeItem.dto.TreeItemCreateRequestDto; import com.chukapoka.server.treeItem.dto.TreeItemDetailResponseDto; +import com.chukapoka.server.treeItem.dto.TreeItemListResponseDto; +import com.chukapoka.server.treeItem.dto.TreeItemModifyRequestDto; import com.chukapoka.server.treeItem.entity.TreeItem; import com.chukapoka.server.treeItem.repository.TreeItemRepository; import jakarta.persistence.EntityNotFoundException; @@ -15,6 +17,9 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; @Service @AllArgsConstructor @@ -33,6 +38,47 @@ public TreeItemDetailResponseDto createTreeItem(TreeItemCreateRequestDto treeIte return saveTreeItem(treeId, userId, treeItemCreateRequestDto); } + /** 트리아이템 (리스트) */ + @Override + public TreeItemListResponseDto treeList() { + List treeItems = treeItemRepository.findAll(); + List treeItemDetailResponseDtos = treeItems.stream() + .map(treeItem -> modelMapper.map(treeItem, TreeItemDetailResponseDto.class)) + .collect(Collectors.toList()); + return new TreeItemListResponseDto(treeItemDetailResponseDtos); +// return null; + + } + + /** 트라이이템 (상세정보) */ + @Override + public TreeItemDetailResponseDto treeDetail(String treeItemId) { + TreeItem treeItem = findTreeItemIdOrThrow(treeItemId); + return modelMapper.map(treeItem, TreeItemDetailResponseDto.class); + } + + /** 트리아이템 수정 */ + @Override + @Transactional + public TreeItemDetailResponseDto treeModify(String treeItemId, TreeItemModifyRequestDto treeItemModifyDto) { + TreeItem treeItem = findTreeItemIdOrThrow(treeItemId); + modelMapper.map(treeItemModifyDto, treeItem); + treeItem.setUpdatedAt(LocalDateTime.now()); + // 변경된 트리아이템 저장 + treeItemRepository.save(treeItem); + // 변경된 트리아이템 상세 정보 반환 + return modelMapper.map(treeItem, TreeItemDetailResponseDto.class); + } + + /** 트리아이템 삭제 */ + @Override + @Transactional + public void treeItemDelete(String treeItemId) { + findTreeItemIdOrThrow(treeItemId); + treeItemRepository.deleteById(treeItemId); + } + + /** 트라이이템 저장 메서드 */ private TreeItemDetailResponseDto saveTreeItem(String treeId, long userId, TreeItemCreateRequestDto treeItemCreateRequestDto) { // 트리 객체 조회 @@ -47,7 +93,16 @@ private TreeItemDetailResponseDto saveTreeItem(String treeId, long userId, TreeI return modelMapper.map(treeItem, TreeItemDetailResponseDto.class); } + + + /** treeItemId Exception 처리 메서드 */ + private TreeItem findTreeItemIdOrThrow(String treeItemId) { + return treeItemRepository.findById(treeItemId) + .orElseThrow(() -> new EntityNotFoundException("등록되지 않은 " + treeItemId + "입니다.")); + } } + + From 2f80ed597cbef7b8b8befb655210df25b86e9180 Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Thu, 29 Feb 2024 23:38:17 +0900 Subject: [PATCH 12/25] =?UTF-8?q?=ED=8A=B8=EB=A6=AC=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=A9=EB=B2=95=20=EC=88=98=EC=A0=95=20jpa=20@Qu?= =?UTF-8?q?ery=20->=20modelMapper=EB=A1=9C=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/tree/controller/TreeController.java | 1 - .../com/chukapoka/server/tree/entity/Tree.java | 5 ++++- .../server/tree/repository/TreeRepository.java | 7 +++++-- .../server/tree/service/TreeServiceImpl.java | 14 ++++++++++++-- .../treeItem/dto/TreeItemCreateRequestDto.java | 1 - .../chukapoka/server/treeItem/entity/TreeItem.java | 4 +++- .../treeItem/service/TreeItemServiceImpl.java | 2 -- 7 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java index 33b4a28..40ba0f3 100644 --- a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java +++ b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java @@ -6,7 +6,6 @@ import com.chukapoka.server.tree.dto.TreeListResponseDto; import com.chukapoka.server.tree.dto.TreeCreateRequestDto; import com.chukapoka.server.tree.dto.TreeModifyRequestDto; -import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.service.TreeService; import jakarta.validation.Valid; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/chukapoka/server/tree/entity/Tree.java b/src/main/java/com/chukapoka/server/tree/entity/Tree.java index 16e3422..96d5599 100644 --- a/src/main/java/com/chukapoka/server/tree/entity/Tree.java +++ b/src/main/java/com/chukapoka/server/tree/entity/Tree.java @@ -68,7 +68,10 @@ public class Tree { @PrePersist public void prePersist() { this.updatedAt = LocalDateTime.now(); - this.treeId = TreeId(); + if(this.treeId == null) { + this.treeId = TreeId(); + } + } private static final AtomicInteger counter = new AtomicInteger(0); diff --git a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java index 64beeff..534ab93 100644 --- a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java +++ b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java @@ -15,8 +15,11 @@ @Repository public interface TreeRepository extends JpaRepository { Optional findById(String treeId); - @Query("SELECT new com.chukapoka.server.tree.dto.TreeList(tree.treeId, tree.title, tree.type, tree.linkId, tree.sendId, tree.updatedBy, tree.updatedAt) FROM Tree tree") - List findAllTrees(); + + /** treeList 조회 어떤 방법으로 할지 1. jpa @Query로 직접 찾기 2. jpa로 모두 찾은후 modelmapper로 맵핑할지 + * @Query("SELECT new com.chukapoka.server.tree.dto.TreeList(tree.treeId, tree.title, tree.type, tree.linkId, tree.sendId, tree.updatedBy, tree.updatedAt) FROM Tree tree") + * List findAllTrees(); + */ diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index afafd80..8c5b916 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -14,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.stream.Collectors; @Service @@ -42,8 +43,17 @@ public TreeDetailResponseDto createTree(TreeCreateRequestDto treeRequestDto) { /** 사용자 트리 리스트 조회(리스트용 모델) */ @Override public TreeListResponseDto treeList() { - List trees = treeRepository.findAllTrees(); - return new TreeListResponseDto(trees); + // 1. jpa에서 TreeList를 만든다음 @query로 찾아서 가져오는 방법 + +// List trees = treeRepository.findAllTrees(); +// return new TreeListResponseDto(trees); + + //2. modelMapper로 리스트 조회후 맵핑하는방법 + List trees = treeRepository.findAll(); + List treeLists = trees.stream() + .map(tree -> modelMapper.map(tree, TreeList.class)) + .collect(Collectors.toList()); + return new TreeListResponseDto(treeLists); } /** 트리 상세 정보 조회 (상세정보 모델) */ diff --git a/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java index 6e9d5db..1e27515 100644 --- a/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java +++ b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java @@ -1,6 +1,5 @@ package com.chukapoka.server.treeItem.dto; -import com.chukapoka.server.tree.entity.Tree; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; diff --git a/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java b/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java index 86bc9ee..bac7a88 100644 --- a/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java +++ b/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java @@ -48,7 +48,9 @@ public class TreeItem { @PrePersist // JPA에서는 엔티티의 생명주기 중 하나의 이벤트에 대해 하나의 @PrePersist 메서드만을 허용 public void prePersist() { - this.id = TreeItemId(); + if (this.id == null) { + this.id = TreeItemId(); + } } private static final AtomicInteger counter = new AtomicInteger(0); private static String TreeItemId() { diff --git a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java index a3d36f2..4afe406 100644 --- a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java +++ b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java @@ -18,7 +18,6 @@ import java.time.LocalDateTime; import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; @Service @@ -46,7 +45,6 @@ public TreeItemListResponseDto treeList() { .map(treeItem -> modelMapper.map(treeItem, TreeItemDetailResponseDto.class)) .collect(Collectors.toList()); return new TreeItemListResponseDto(treeItemDetailResponseDtos); -// return null; } From c2c12de1f1b171bae5219d0595f19d4c7befe780 Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Sun, 17 Mar 2024 14:47:54 +0900 Subject: [PATCH 13/25] OAuth2.md --- OAUTH-JWT.md | 58 +++++++++++++++++++++++++++++++++++++++++ screenshots/Oauth2.png | Bin 0 -> 177925 bytes 2 files changed, 58 insertions(+) create mode 100644 OAUTH-JWT.md create mode 100644 screenshots/Oauth2.png diff --git a/OAUTH-JWT.md b/OAUTH-JWT.md new file mode 100644 index 0000000..6a9ca43 --- /dev/null +++ b/OAUTH-JWT.md @@ -0,0 +1,58 @@ +## 구현 +API 서버 형태로 구현을 진행 +- 인증 :카카오/구글 소셜 로그인(코드방식) 후 jwt 발급 +- 인가 : JWT를 통한 경로별 접근 권한 +- 인증 정보 DB 저장 후 추가 정보 기입 + +### 버전 및 의존성 +- Spring boot 3.2.2 +- Spring Security 6.2.2 +- OAuth2 Client +- LomBok +- Spring Data JPA +- JJWT 0.12.3 + +### OAuth2 Code Grant 방식의 동작 순서 + +1. 로그인 페이지 +2. 성공 후 코드 발급 (redirect_url) +3. 코드를 통해 Access 토큰 요청 +4. Access 토큰 발급 완료 +5. Access 토큰을 통해 유저 정보 요청 +6. 유저 정보 획득 완료 +7. +### JWT 방식에서 OAuth2 클라이언트 구성시 고민점 + +JWT 방식에서는 로그인(인증)이 성공하면 JWT 발급 문제와 웹/하이브리드/네이티브앱별 특징에 의해 OAuth2 Code Grant 방식 동작의 책임을 프론트엔드 측에 둘 것인지 백엔드 측에 둘 것인지 많은 고민을 한다. + +- **로그인(인증)이 성공하면 JWT를 발급해야 하는 문제** + - 프론트단에서 로그인 경로에 대한 하이퍼링크를 실행하면 소셜 로그인창이 등장하고 로그인 로직이 수행된다. + - 로그인이 성공되면 JWT가 발급되는데 하이퍼링크로 실행했기 때문에 JWT를 받을 로직이 없다. (해당 부분에 대해 redirect_url 설정에 따라 많은 고민이 필요합니다.) + - API Client(axios, fetch)로 요청 보내면 백엔드측으로 요청이 전송되지만 외부 서비스 로그인 페이지를 확인할 수 없다. + +- **웹/하이브리드/네이티브앱별 특징** + - 웹에서 편하게 사용할 수 있는 웹페이지가 앱에서는 웹뷰로 보이기 때문에 UX적으로 안좋은 경험을 가질 수 있다. + - 앱 환경에서 쿠키 소멸 현상 + +## 프론트/백 책임 분배 +1. 모든 책임을 프론트가 맡음 +> 프론트단에서 (로그인 → 코드 발급 → Access 토큰 → 유저 정보 획득) 과정을 모두 수행한 뒤 백엔드단에서 (유저 정보 → JWT 발급) 방식으로 주로 네이티브앱에서 사용하는 방식. +→ 프론트에서 보낸 유저 정보의 진위 여부를 따지기 위해 추가적인 보안 로직이 필요하다. + +2. 책임을 프론트와 백엔드가 나누어 가짐 : 잘못된 방식 +> 프론트단에서 (로그인 → 코드 발급) 후 코드를 백엔드로 전송 백엔드단에서 (코드 → 토큰 발급 → 유저 정보 획득 → JWT 발급) + +3. 모든 책임을 백엔드에서 구현 +> 프론트단에서 백엔드의 OAuth2 로그인 경로로 하이퍼링킹을 진행 후 백엔드단에서 (로그인 페이지 요청 → 코드 발급 → Access 토큰 → 유저 정보 획득 → JWT 발급) 방식으로 주로 웹앱/모바일앱 통합 환경 서버에서 사용하는 방식. + +→ 백엔드에서 JWT를 발급하는 방식의 고민과 프론트측에서 받는 로직을 처리해야 한다. + + + +### 카카오 dev 톡에 적혀 있는 프론트/백 책임 분배 + +구글링을 통해 카카오 dev 톡에 적혀 있는 프론트와 백엔드가 책임을 나눠 가지는 질문에 대한 카카오 공식 답변 +![Oauth2.png](screenshots%2FOauth2.png) +앱에 대해서는 모든 책임을 프론트가 일임하고 코드나 Access 토큰을 전달하는 행위 자체를 지양 + +추가적으로 다른 자료들에도 코드나 Access 토큰을 전달하는 행위를 금지하고 있음 \ No newline at end of file diff --git a/screenshots/Oauth2.png b/screenshots/Oauth2.png new file mode 100644 index 0000000000000000000000000000000000000000..a825fc6ffdd594eb781bf1f6f3cbd63072764a26 GIT binary patch literal 177925 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 YwB2nI;*%i;)Rwc7lY4x&?_8{od}Tv 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
  • !?N3D`(0|?`Q;1YYu7)QJc|a$VYBPh2}u7jrJ$z%>seo89W7*;_@g4% zZJ1@)<2zi?+_Q)lnl;mHSb08U-GMU`ay3qR1#TR2nm+^EuWchIU7QuRR@~ik-ZEC$ zTSnJqS1`dNPPLh56Z+~tOJslg=i!CAP+JUg$4q-|zUYe^UguneZL%Ll6Dk9xO~TeB zO&?FOojEx9g_Qa(aWGS;w=+_~>be1`sYE<6S5X)n0=&Fo2PvRVz)Y=yt+eQV_nXaj zwK=?szxw6ryWcII;Iq3bK>N|*6ygjU{xI;ahAS>weTrd`4{@spNiU=uRQlpHHXo%K6$|G%kela&9zF0JA@C<9-NwHtd-O$SO_sH$R$v5e0a)9MjD)}aTa!u zgDLI#f~SZxTDCjrYP$A__TcjHOrTr}O1{G(U>GVt{=Ct7%Gn;Q=`J$Ca)Z)5g66^bInfg)6;+A9g6i0YUej!;}KOJnqS z2;cpQh2$L3Vf%*w1ItivpCSMv4+QID8Wd>!4kb~zOlgsv{TxWuD9;|+R4aWxoDIv_ z*a0$EBF8Kc#q`?t=VL_r^cb{>(_x+wMZysVjvb>&2L>5ic&OlR zY;CuD|B{I@VL{`~&e`;yqTkPJ*U+b3LUlTPRW@#}CGmSWq~syAYeH&xhN0*AfR^;{k#ec>ZKN<+smdhoIPVl&&0(ys>LUp}cHh-)KTar+*4&+r0)+PGN9(;kmz|fJ5ofRn)GqaBcCaxnsVgGTX zoJ|~>1{KdOU((b^UDii-0@R-{_MGA5m|Vj#n$bnWan0P@*r}}{8-hFa!XPboNi5?& zYGJX5HsutHJ*{rTmkW&1xvy4{IF>lDL@p8x%%&I;utFkH7_myUP)kv8HKSjEIJK+0 zPe%hFP#280*GB({EE^gC&y@Nr=We?l#sgDj>V5suZ8KixHRjwP(H|B%Vq4JI&hEl% zS1x}sQ)7&O4n&cytaQc7{{oPUsd+Jy(d`d$*W}P`BGzHM5?s5uzz6VA z5*`Dm3$H>KL{KM4?OZq z>gM+a9e&>lyrdU~LT=G=*4sdEkvl(F>G>NGWa4^yO1fH1B(2WA%H#Nj)`HF#-(mMV z+t+ff1vqm>7nPLwYYefomROa<#3cm1XbYw&IigWPiF14d202qogbSR}zr&&q7HsZ? zZhq%_x=+K1?M!j^KCs$uB>qE3g%wvodR+dKf|?cHe7LWnyT_s}RPj?jC;JZSCYNsq zgJ`%>&Vm_#wrbqj?aAMOEVRA(uOn&z;v(;*Z%nX{ZW-Vjuuyxkw5U~`T;(0}ieY!F zZFrx*5ALmjpBX{Yh-YCp`PJrEuwVitT*s55^~zAtEUp$Tzv5esxWU-=wq#?DV^A=u`RwxGG1F0D}3zZl##t6(ACp?bwe^p(kED$^uBz5 zT!oER#<%VyfCmxhZp0iZaKMlXw){l2S0KYFC<<>EZS~H?%l7S0k(r*~%J-RAM39PV zW#n1~TU=2UkALbUdsNvvZ2?MkaSyl(Np9wNCUgJ+xhW(|D zuZKo#up=j#Z!<^zzdRCpAHR&41R$Ewl!?I|mC}QbX1CIrAV)rXzDENdea!sfyaDom+ z9l+0F|6ya0Kt~GicmUPXzc35*>JI>Bp=@RR-#@aUNAr~9?&r789$cKC49bWOHjdSS zYF{DVq=gxJsvWq&{SRIDBk=mN<`rV{1fUrJT$iF1+`y>wFf0185a2`9d6&1I+K61;eW^;dpS8ZI0{m=;24%`MhzV(fURs+m1FHW z=6^QLUXDlnPeICFl)og*6{v-2)SGBPijji=;2x4bU1&a#a%Dyr*i7-yMFU2x-p7xC zFe^xlFjwo#-~Cb52Rvo~&)2Ax^4EXy8cx);IO?1%E|~~;g~{4qQ?9j$3X_A(P5$B5 zELSN0^&jlWWi$95hZ+CO$hRM*I6>=>d29tB39lB^5pmBWnZ5lFOXSByB#Ux27l3si z1kl0Lk&<6{&8f5wsk&b@ZT|(S1#T7_7L!7?Gp|rE;AAiBWf4wu_9w;eLNO;a*D$kX z{O{pQn_{0NtNdNc!|em0G)CWB5=Id(OyXKG`pn zry#i?nD;5UU|TeQUSAQD6xvUm*&H+c^p;+`9)fiz9T*M>%10BTnXrp|*dF zh5S9td6KuFN%G=7}1(p>!m@_AA_|(hb<{{^mh?Lh7kSy5|Ll9iX4AyY}Z* zW0#+G8LiNr?ZKNkO~}{dN!_^5p2K_xKR)$H?WoZqQ+7Nyae&SPgWh#AC$q zBM+sFjlGLQm_e`5&j>?(0QOjW)+}nRQ|wHxqq^m|N_%Q*4ehdnwTk^iz^@Xv87htG zhIE5HlB=KF#n)Z1X`fh#)vWo3mlf8z6Ge5GUgT!BUfG(XjNTvUrBc|Wu%K?Y>Z_3* zT&EwmNK~EK8E17y3GclrJApfl^>9I3^^}UfdS6{*6lf_M+iAM|(NG9Qp|#{vJy7^h z{uGnVr6^}vhI}SHt6%SfZL-GClIH;9)kSfs(ic6m$F?eGSgBuzm<<{Q0rRadZ> zj_OE~j2N!=*iRn4JvLShWhJul9!pI8O_mL$KiF_vLu;s6CzSnKM1UH9_F$EB$7hAB z1vxh%yt$j%*AG>?V+17o^d~?6N}ThanAjRvG2$)>RxEjb9$f5u;f)@2zBbXY`vIsb z2L>`)RPgN=IeGa=hTJYlTCM{Y8e+Yj7db4GyBes69LT%r@%uMXp+GzXjgTwn0m9dQ z#>5s?IcgO-Sk7mk7y#0~Hn@k=zkC@Wryw6p$BkWVc|f3xGMx>#VdR5i+5gGDh-fx* z@TLdhqNX+`rC7pK2kAndxXMQyxgG{4GCvfm5+sw0IW)m8?cx)D=Er|CUl6UiD#--N zf-xXGla@EX#*KCqhe}j);}{L41C%)1W^r}qRE<>#0ddHie&1nCWd=+p*Ev0^+IgVU(^&EG zcaw9*!z_F>H$K=M&45=oaR#<&usS@L$mX{@n-AoSd9tVZ(?hSNj;ZyR3AYs`zU_qp#f`0s7VN_%?$5STt( zyceJ(BZZO4HgPxwz@<#2t*5yZ2UO{|Yj8YaoJH2hT|<8`MryV&D=fSB0ZNNYxrNHp zn-)utj%2u_=)_9R{PUp}=t31v^2r6-WJ~M!6vF$<^mN{wYL+*4qj1J9B{TV^J|(1| zl=Q0<1XU}GvH3I1jSZ=w=?ym9gEHc}>(KM+4~ojl5UA+Z8ed-Dpz%l@wNT}ZaqwJ{ zon=tMf-pwrw-Y1u;$cgU6qo=eA9tR~{N*^M^rR2ku6KpZ@TaeT-NU25rePWVjZajz2tOrtf_}Pf+eQc5=xd!uvldxzIEpj;qQj%#wcVc(G4H@ z|IkBG4t@I*v~)XSjXF@M?X}&r`PIPH*XC2Xq4P-v5oSy@dWf#5Q5s{LiKKqi1@UEjfo==Op7Dvve9L$w&t-1vi*S2`HmX(tNrXTY|HmG(=^2$c+;;U`P zC@AycG%gfRb}jWfo)NqxzGE-FS$Fz;%znKwm{eW!=JlHgEJ%P5vcxUt!&KIdW`itZ z5TVgeS-3`l-A_hE71Ejp{&4DGqQVt=L6$2Y7nr@M^ht-ob(D^X846C$l~^d%L_qZk zqEQ=^hmLZzw*_UyS_Ja3ZBFKj&V7Kpld@5I;Y*kcpCr}<&v8-b{dC%wmXSxn8!Oyr zNPF+?nxV7~EQKpi&bBYB|CqTd#PWN0}C@#|$z z`8enuqP>}%p!EgRfiQXH#|J)M0qrMLPKwrpE~b%3ml%4eHmfWJ%mQhv z=w$v*caTdi+5F23>VtRW@roQg<`5`3jhA9W=^Y9(F;FF&3N&6amF!6ngIIi;0>?Z} zCUr>ou6`ZM5eGRqf8?4|!R-J3j2kHG9k|Hr$!>6LH%2MQ1xL3O8Z%CJ3~fPU#RxNw2}4maWY6VfWc~c)7L#MNESikv7I!mE z%%^*+2_AkQys#n|l#G4uu4f^KB%5T+ zs*1iM{iCBJ*uOzQ?S;?b)y*&tO&zQ$T!&g0%SZF(S39?)fC!D5eVHIpV`ibz;At9y za>k>gI!ZSP!$lT<8U(*$(S-GjL_QtaH)|li((!e-Q9!IxP)BZKw zueivzP92813zltsGfQzY#?F&Apn_rCo;YY{| z>tJz;HHu%Z850)t84A&XQ(~4_`V_0tenW~h@@Sf)h3x~sdihvVLm!_&aq`9RK-c^E zms^RaTU;G2%uJ!Koc?1epiieQNz)B{m@NC*hu+9#Oz)G~D;z+6Dw3!TCDlN_YC#hA zawbO449sb;>U-7r2Hj-NJ~!n!Dhgr4-*$}x^*a)cxDx46&-5cR+S zz0nfFi>H-eHG1ds4ejxZ4cplF{V6VM8d9Y{0}EgHDEa7M&6A{YyhM@-XEzq(KJOFZ z6DRgc2|-OVAnpcr7%J#IRD~rw0XcV*CQ^sbm09OuoO}pgXI#{~Czd%2Fr&_-r@?q# zyLR?AMdRN=t5>$FV3wd=OG3+)LZ7)~r?5>51mvaYIwCm(9=F@!m)xADrWQk&F-zw| zxsc^aiio0wp zxuz0WE=YbGiV-%0)3rlv-#T1aeNKvXnaU|B9N5r;)JF>>E=r5o(#`ce8hA7J|Ivj8 zgl*PPX7+Dk$f&&>9<_Y%y^>mA`wFkJ1=t?y;tszs$Vx)E)MDc>U)ji7x@jX26E}b7 zxHmVxWv4>=H1Xw26ROH-``^w;7S|mr+Hxf9l0Biw6m9IX&uxjHX9o;rwLDod39WVQ zfrPwl{dyq9z*bz=irKjZudbmnS84t$mJREi&$Yjfe}p<*_6iCLijloNM!t_nMfoeC_GRH0DzgY!M$U5TDEy@3Vy++nk8 zMq|aZP}{14+kYkSp7p4R%0)_7J1F&wq!jbq{CrsQG!-=?(XhZZP=Dw^uaBo@p+XV! z_Cuz=JpO4M?Ygi9QulqwlW_5kKM2}bcuQ5q;)QzBGycFtH!#S-NB3>^@^H*zMM+E_ zhuHZZp_BwU)YfdMRVQR0smt+#_}&}#kmeCgXbnmoOPKH z&L*o59G*3~(YkXvC53fs<#s0-spve0hV3XJ6$wZGBXgQgzbY;Nw3BOd)2-2Vq2I)inz5g$V{*`41KxNbXts z6>(ZQRzF1+^`K=(AlvoRrD4RWp&yHom{+ z!Vze^b2OOZt*l*{PTC7Jkd&u;T3ctXO{gluk-j`N!fP#ZnRr5eToJ>yMS5IHfUQ@# zb89(4^8tVvb`@lbcgO_A~O= z&^Ti^+`(@>X^h+YS<1??Sm&=wLo<1~s~CA3HMUfnH@MwP=9Kezne4iCv~niL1(h~v zst;@=tfMrqiJ=VR4E=MG>|`sgFwY-<6UolO3b&I7^OoLu+4#OYo93!&vi7Ij=r7J$ zXyN?c`SJ(|vo!uKEe=(XNH;D8m064M0&gWGTf<+~p`>O=36-C0fr zL>Zv0ySS$2xOc0P09k=pS3_+##X|izmE=-go4Cx%rB*UG#MuHN{X`IF_sgH-bE zHDq%mziu@>FEWK{i)(GeRGNv3Z&~V`CQIE@K<$eOY8VGM=e1cHH;C5Oc1t?olhZ`8 zO-J-#UR`60M`SlOve$hR92PqDrY`ofv7y1(!kOe~SEL(yl%KdlG4Hr^!~=^n;<7|2`*-6TH$lsAQb zBP?ve_5qY$0k5DiT7}VFqbz-Ha>DT4Cvh`q!<>ij+Qira(J2nboh%U`Bi|S;PJVxS z63*)bZSJP)HoY>uo1luDS}BnF#h9~DBM|j4qVubEnwKBzji90+eb^)6@MXYHLY_G| zua5Hl;}lH6nXmO62zjUL&zG+~>G&Tw9^rdM!B1518jceqTlDaP;MFx`pu2$5PC8U^ z8q2A!ZNaJH{$=~GEME(K|B^R&BY+fBW% zqCiPinlbHStj3w?aQW=kr5c_bDkpPIv;(0%4*r^1^_x% zm$BV1nZDKKLKX{i?p)L`_42GU<|?MY&5ulDS*qi6@N)-DVZsPH=dn|t9M;>tBJ=jD zHP)ePL19a+ey`N6*${^lwJB`vr(N=s_(|e@rqct~PX6vAAOp|dq181zO zdH>CEuSl-4q??FXF>K!Dg^PTH5BGTZSB1V$kPZC7S<<8Z!4xwAK3;Xbm-vz-GjwKC zby0{}3o5dma+?IWnTirnDB1|hBcu#wn(kyGuTETo?k&<0wVd?Pkg`t*oXd)J1EupZg;0nP;h6H38^!UWdR`I_#D|`v36G_p9h+0px%;6& zfJ62-_1`toU;uFaPij*P4-2ss!v9Te0vdjLz{IqYRlh0+ABY zw9^QOK;vd9N;i>-{~(&aJPwi!dX=p7dG8+O-Nxwp=BUE_lpt2qK#UJzC7EzP6!g^2 z#~&CS?KK2s0B_NwhgKm|LG{si3PXNdLI~GnI{J?v&@6}gzRiU_vM5v|oLH-EnP6g* z1_Z`CJ5Gp_SJz5&Hk(d z^&j3O&&ip&dQ4^i?0Fqr^Ac^0$vet;o%LN_2we>&_PYJ?9`lyWZ*NR-Owmmx&62tJ z7c2WnT#ocv`|Z1SSP?`|tgCk-a$o=25V7bJoiNv+?3R2(QA{`N%inc|!HXteqaa1!} z`7=ZGxMlXQA1GSdMI5Qu zP*Y^81*q2bp0|xf_-BJRHz7Q(HWwP>pVkV)q*+GTQ6%&vq?u zNf!4oZ~x#%D0BMra?gg2OQf*U|ApJHWOuf4vs(D=S*a^G)3;0WwkGsX7L{fc@$0Rh ztm2AxdS2eHi3k60Kf3w4l3GMqP|y%N3uc82HRe$F{*f7|8Wa#BwvIhc$W(K&CfvQS z`TY-|)?D{_jkIT_jXo*-9P_|d3RF|ILz882`%TyR;YRKGOVi_rkxG9tNh>aU4uWG7 zWdd*Y^Y9wsMh?5D<*-FxudGm5Rl8-6#*sfMAd+=3hNR8Th}(#^>_%qW9V-fM=l4-I z=eo}I2e@Yw9*KK?!HSJj zk4{MZhFRbAaw*M;>j^ExSB>~_?_A8#;q%D17TZ#0r;Y`tTgS5;tN2MR#!sG)%WHA8 z-}R@7$gA#+3XvkLn_qY}=|Ov6c*~ej(DkQCC;$Q${Z9m6gSL#kmqga}+!-o4DM=?k`1(VmjTB342S*dEQ)fh zRxpu5(hd`kxmKPQkgdRxX!}vn9UhnYivJR`R*n<&C9}!a7lO2!giZ&A70cW|$Zc0D zPWHOkMoe_2S|-Wk03X+#Z7r+3P`Gm~`aPz8Xp?8ceyU(H?Ni4A?4fyz0U@{snq=A>6B@O%TAx$jXMiJ)c@n61A2aLOQ zo4vpM_CY2$%td(piO-6iZ)qc+-_z_#+R!|{D-KUMS9Z+((1(Uuxs|@tbpM`o`5VJ8 zPzEG8+NX!^D2x<@GZXrtXb6K4Kv1)CZcNb1>Ycyj-yC-cbfggwfU+7Chn37=c zzF)r~X(mIW;e27!{Gr)8D^f+^s?7x5Nr#SDfZhTg1k!iW`W(juLgsV34a)cvlj9AE z@(@CckRc%4UMWcV3n(Sq{N%#?4IxF^BiEi7??EUjrFOQa9Ahg--a%YWoDVCW@~x|@ zdsjR%(e<;na&pd-uROn;lvyaoZZd?7I@Yfvk~RPtetFCNhD)Xwbn_nvpQ^|K2D zbaVHsU%GNG=KDtbwdqDWdB?bnWFc&r>0|7=?*}in`vLPc;rxiOVCi#0*GsmMqkUqM zq^b-Xs##5beol>Zl9Frjyku)HCrdmV*7712_sEbalc@!kQ!ghMvX_(0PWrc5xF@7i zRnv_^->l!XmTu*Z_eR;VVs7Q*gCdOav)--ZuH86u&tmfssw%^5=6jTJB^dDOhO zz@j%cXxRpJM7%mMgEH%!BoV(vNAo{8`!?VzHWfYeZFFWj>k{!MI0pOFrdH?cGs}nenG0>|U zRc%~gM|a)ysFG>)fu2kZ$cQ$Vb!;`}CI;9pL2Ydi>jRj`1cfjNdjW3c`H|R~jFc6z zj;`)G-7a2iiF76(Zok`Bt&x*p-wE=(-fs{03WkE5N$ATB90L*YhxYlL;#~4a^Rz!d z7N^Iu{p{)7e+pUC?cdyTmf?(2Os)h9IC738?L<-UPU$jhFe4eg1bF^!*#J5#ct+OZ;6gE!of?cG5!%|sY}!PLf4 zCmy$OBjp+qG){Ww8Tj4RGM@wac27=QWjw0|;#7lp!1`_L^W5*&2{Q3Lw8$4V^qMDM zqJs|k32dF0gp`-idHr) zR&Sksg{#s7(>UDtoGb6plTlq9X1J7O8{J!#u2p*lMs!r~CM#BVEo^5rT2T zCx|`KQ^*J@CPKSB*s`mEnmg&nXgj(lkjk@2Pqj$DUnnfe(w2kZ{bVT&rhCIx&}!DyE(iFiv!*#hzBVoUo!%HCpm)(;d#TZtX3nRFOVXK_TpzTg4v^CS&G!e7<&!lu+Pr zX506$UupavB`1?0c7JYCAR9_4E@oh1llC-co4XkUi}W`<^R~HP{e@i$3H-HHmBsza zc-k=(GVCD$KBnZyydGmIb6o^|kam#Jg91}kfA~uRw{9%YmGuer;}G@bki7}6 zA(*dv#0Kzl8oWs(WcFkq7vMmwCVuF=JI%?f7`ez(RVP2Uq71!*8|~JSFy9X>HzG=^ zn0N%HxFP{_%K2a1tIb8WBoSEUD6*Zx-)5iien>~`20Lv}ZH(zF|NSveAs1!D)eRCBqW$IuvdyZ_+sLP72!!&AKQff%IX)k z#26VpMVxw8qS)ADG|Si=94sk!_aJ=VXWhZ2{q@d(j8;xjvg{_+PtaSW%~` z`;1MXcE_ih&q$UpeSt$xCJ6T3!shvz<{)>14HoWKUVbAe*lL6}T=QENznO193+~%! zC?pB<_Vo__s;JQ)^YZ#0EDZgLqqYyaz=3ZT$5AhP01z&D8#azy%{Oa+jfGI)-h-i3 z)yT_mH-Pq5C71FF71_+N666GC65OLkP8gL0m1E#GkVHRzA#sGOuhM15W-PelR3YlR zc%;GYM+My{CoSxvJ=-#ra>Zz!TwIJM9Ei+K^Eg{NV46G z<-1BarSU)ngI#KTsj#S8YXAC@8|_u{mq5dCzYRc<)Th5J*pBa&IsIIx?%=4$BE#R; zNaCn+PA9Nu0mlH6f|)iDlkoPWJUslODtXvoYlL?=Hx~WfRk@}^lQYk_<4a(OaFo*) zxugJo)AvvY+c5>u*&Wu+ZEn8nRgY~g!rRy16m?{tBkP+s@p9E^!f`U530kxN4`*-r z7RT0g{U#x}hsNDCxVyVG5Fof)aCd?>G#&`Sr6EXw;O2M-Y3-95aOz3;v6=Q-zk z&xi8|RA1d)t5(f5=NP|X-^0os_uy>LczW5uF~<|N9r4Zm*{r|};=*r?Gfrq_Ln)>Z zf^5Ek@XcAa0nqoU_!Fs=XqMt58Dpa;xe+YS@u(^`??e~4tN3}Da8cAH!lem6sP$8v zA*#bSU-)$&1y5~uhqNX8kcn4KlS^p<-LXfR1R&n{MD289qqAp3j-@h|+YAQM8(O%y zk~LC78$NmZ;MM3B1?RyTOFi-)r{B)R>4y2^Tq(accnd#zrx>Y_MgEC!PGf^&+#hzI zZBRh%1jC5kZLW|7uJhh5qZN`w(?M-3bh&JOCI^X`bC&n1 z@V|QJ8OVZ!UL$}uEs%`n#-Cc*lP)V;6S$$88K6K)N~XET!bj+3kS zPzj4A@v+qPEIg?Y>%e{N3~ms#9wa;YEj2vVzgFY4vEZu^7KoO-9u20VU)@RRk795P zWCK6!%yxwIUtz?xaXg$DvJ7i;xf+S~o;T_YvO1v`zCPo3nESPerD0HBUT&)20@oAF zn82s~YeH4J^hnC|jZS05=sq1LoKiBP?6U#T3hHo$kZ){Yvt`bO=Us%SCeIZGhq5BF z5X7-jb4wnw#dgRQ*>2MkB=|R@t}Ts1b*wyKxw!xr2E>MRy{1Z#0k>jHfLauq+%$ig zn8-?fwY`fuL&}0E)+FW4?DXEUQbN7x^iOWvUhvcW()4v2j1P#3`v}MX8z{<;3~2$c zfTe{(%u3C>vGraXYg{HG2rJ_=3trypUz?{BsK@khp*^q@y6wAPU-9%_n@5wJTgxBn zCMZ)M?Dg6h&t2`h2*^6Fsp&3$%sH3dzygo_vKc=h^g(7zl+$FE-63~NWSIFnhMYz^ zCpv}O+OHP%neu*`k3ELxusv4=uMomk2KhyadsSwAeG}De@!)-KG}m^tS*lw+3-e+s zEQ`aWZ1^IU8q4^yX&`&*{_cT*Gig0snRhb1=G=f4Y8wxgOj z;x}^N(;!Qz`@iWljsjwZ*npOoK5o@+U!b_^;JN++U4I|o(xH2c3-jj_FbL*Lzy6agU z#XBVNb9yz$C5c9bSY%ML7P2fAlpkNpWK;}E^%m|c+1zX0%w5bPl`HUFPc`nSHcc)B z*=OEHOQr|aG^Y<&yoXg04(W6EL5cH6HsyyIWnKsLY3W0@wmcd9?s9n|!5F2UWVaq! zl2^#}st_4S$N{&q^tXUrF+tfPDokQu!^WZ;5>{sD`3JMS1L9Y{V?%Gz-Pd5Yhn{tQ zV74{j!EGWW@DNJXEX~7^GU{J zTOs9k+I8ee^=k z_Zh$}+u>>3xZ4G=M(__nqyQB#qp`gH?$USK2`T{m0&@9Gsrp>>hexjV zyY6d9Ne`Qug=`!|__{}TnWY5u_eKGNeB4cbYtIvqa5tvj8r@KuppG?Zsl=4fd z-AiJ5DJhXVx#KACL2GyY70-&WH)7gpMUNld*d1ql& zAy1F~!OO!IT@bM+pT)iSQK{QmK*qq=E*kEzdhYYp``6Zi*s=1{-rk}F*!2Ri(L$EW;<|Vs8-822L`=|#Z*r3d5lk_)DW$dru`DPh#CMgeX#{0<- zSb=*jT5Q-XToc>yJKizdN}tM>_LyH+vNWx6a+dcDXw51hs{#t1hL>N`WOPXr4{<;l zfy9h13IOB;bKz=g7_Aoj?prISP9BGpGzalW+$KXxp;~& z4I%BLjLOM<#UYXzO)$#oj`R%f|1f6)TS#CfCIBmu)xokaZ17t@$!0EXAUH{NEwSaz z7^;HEThIo-vxJFCT6~h|J31RCNM1V2?xW%o+Nwk}p;^2VS@ge2T~y9L=vjUrP>v8F zVrY6V{`P0wuMb7z@;{x`{N`MffBmLBAPZ~%Ejp)x`pfOMHF+XUfQp;dE9Oe4C{ceV zd>x4thgy=z64iXIXU^)^z}gR@6g)f^6Wj`^j9c-YEYf;|mk4@895n=X%vIz&?Bn?D zYLX&olUgpT`^anj;y$VvdQ>d>A20Fj9B$gW5375au(hVc|X)j zGssb7E=CaT>RMVig_phGMBq4(s_RQu_fl``P?Xt^8@-)jDV4GACQDRFB6h=%GU_># z6LGv{Gbz)i_Y^;36VpCd@`0@zITqf8KXAg&)ZVS|$SrS_$Th2L8z~VfBrdyZ?i8oG zZTKQBe|@!7;8Mt8a1=-$%Z%TrF*oSxE~RlF#;w7E+Yj=QPJJxQac?hWIC&#+y%`EM zi%ZCs7d;f-czx7CcQAsq{dl8Kmai^4)I+4%3(L=iYl!dK18m{(qv$7pOct73a zNA`5BcOCTltE72^N3D6H8SfX;=vR~IoOzwFo?+3hmw=F&D{mOZ2RFP;aVPMZHuEYQKQ3Fx5@i~Cqn)|Q#T2X99;F4!G zJ3x|ZqA63tu3}oaBoU9*h5#kuk6g*%P!P~mk>pny%blt|eJ><^L&*TbKi zEMp6mu77&Y?Wz}k+WkiK>;9)wy$sH_@tjcV<)Y`?JU%^E!|NdAZgJwX7_b)VRN+&QNh2--0K_i+S@Ve+y`f7JOP}J*%%dJ zbt;mMA-lvqnLS1k`+HfBeyL%5qKT{r0VY|m?iseebyqXv2BokZM(G;|)eEXR0Lctl z9a3LZCC1}<_n}q}L;14Q#3NC5IuR9@PgH6)#NUFQ9NTK&W?w5}gZEOuA(0>hKA)Xo zaK4ol0`J$-!Vdq#JL=|}C&yF@Y& z9o4*6jF8LEYa9e#-xpVuP!I4b!bu^F1hsMTMcn<=qckq*WArTi%-HJ~c;z(yQkEW8 zQi6eYXCmM^znx8n@d-bUl-A66vvccqxuJpUkA>nZ;-76@@Fxdg@;HCFlrW_aw29eW z0KY~hQdRAHVGVx|Z(EG%5SADgr%sD{6!T_H7CLIHW~Eck7FzzPGOI7qk>4z)rlw#< z6KEs#5?Fm6w`v?kJQ8Vlv|s42VMYF@a}#-@5_E(>Z$JyN zLh_6(I1$*mvI_3DF4x9l__8H1SmWNzHi0fW`AZ3|`3OjbE1Z-9bnBNo!GP-QZM(9V6$0`21W#Zd!;IS0O3=g6JNwkpdRp?eL2}nZFE4 z_?{UcjH1VMq}uw1BOGv-?2P$zbKTKbC=I%1B^|8JYy{(86&2x@;i_% zXpbA1`pUp+*iS1QaDyUb#k9jp4;3kJNqk&GUNdS7fmsXc%DIABBwubiENDPg9O=MjO+HMtnFEOaR7ChVa?yi|iWZe`O&a#|x2Qo%W~q!GI?Hd)!u z29;=Hm>I>S$ZLGGkF@Q;{Qkk4p=dVzv0WbI7iDtzjzLE8tq__YQ2|}M%)|{WWPfZg#lsn61%&442~tDN;#D9&LeRJy>S1pr_4QFXjfMMKc@=?B%AQK- zVylMk3-AhX`1H+0CFa9_aJeU^_30RxietIUO zL*8FF)$L+FRPx1m9qdYJ&#F!Tw{>Ux&KbZE{`|upv&zm@e?Ba_EBm(oPY`&!86Jn# z@@KOfCKSJq;{mt)GN*3?G&%Mv7s4KM$0L_bbAQKwD>4AJWO>@#q*OK5hGZ5%@S$cc zff(1vt-&J!biUkf`3#OWxb4O)(1w$}k$3LQe&fr_YBpW;LD%of5T#u=Fmw}fF|w$to_2 z&i`^`l+5$f)(a0m(LLJON)?DgeD|V(4f^v1=qGN~>?e#7yEX=B$AjF3N^wU8;sdvL1Z7T$FeDTDnJNH)Y70D4}%%e)mr;?O6IZ~-8O<^?+{~ue^moWG1dYWIb(&I;D^IfmWvO~dhssYcntOlB0X2Kxf!UHyh{m$R zO0c~cFY^Wt8ckH$)`mXv{e@Pj$T*3&dJr=B;9PLRFg^0@t3qROv8Voo`iIymcnn6i)YILEY0U%%QZd3Zr5Dhw}VNvQ^aZ$cVHIZxlpKbI!wt0BB*ww~}IJ@_rqi+k=Z z0JdMuq}=z98;B=|oSgi2crfm`CA)uqAJ$_A`T?u*m@n6M{n|Jd3uq(nNxl8|HH$s) zw?hlFwxG+u`l2^p*dOM2a9c>f0t^?OxH9tvEIo76#G#%Is$(`mDQK%9Y?xROLrcn%iEDG#LnAnF93aCU{m*{e>w2mDiLk1+mY^dF{3+2q2Jc{> z@>@x;GBzjPxNOo8DHSIhHFS31qj$0aD(Q%;3BH@@q}xcbvE27^{nQDgucMo!hXmZ@ z?5~N3)|p)ez^wvH5showsTSM8??=dfP{f3J2B6d#8k)j%8=7JYn*}MEST$nxmjmMt zKBTYIHaGiT#8-rUJQi;Wsc)Z8AQ{o1iZc5&{T{~hgJJvt0PI$nL;Ebm<+|QkTsj~% zMTIVm7oQxSbLx_qQR~Jrn}7w#9drJ;s7EhXfzf?$mr#L}2p1}R^eZKyxvJv}u_nWi zuLevJO$_aNX!=S!T32Q!&f@%$8ijb=U8)+`lmiat9(Z&RRKssM`F3mmU=nmeZ#l=2 zl_p@ft29xZj{iD}!Oa9JE-w$3oQP!o{3_or)@CTOGLyvgB(5=&9ZW(fR)`3)IVM5H z$h~{gYi(vfjZe;o)^Ax&A>V@`^`oj67%K9u6I4@n zXWXoB1_emkkEf*#q8YD8Ay6~gHRJ8S4|AEAqi<}6i($G1?b@8)hzrHsv>8)60H?5* zL%&JbmF4j0c7LOya3q8MrWX%ia(jf2haf%};t>6mBdIC*276I~Po(wp`}IlYua<~M z{^AlXLxD~52Lnee7O+p&sEo%}GjQx14A|rRuwk#!8b>#f+kY8;g!(sf3b=w?Oec@4 zN4W2YJO^&CyVS6tglR~sWDu7r%b|sw&alkCj(VrAzxM_fIUuNng_d}M0QZ}#F-MJ( zdI3zADdh8n`QHlya89op@Bm}fx5=rSOjeBZy0xiPb?W14OkruUA$Sq<40H5L@2AZF zI{4Y#{$2b57v0`8p~MoJ<>Xc-+bg3f$#6u2CedpcstgFYu!q~Z5|xV-;LjitM~MIP ztFuB&7%WH+bm@iaIc_P)fb05PXe5+_jOjUsbMFDe%ir$u(Sb4G6l#@|;0N9HvdS=F zoYU=1^=M4;{9^vgRGS};RwxmVg;u$1bli&d3a5>Ay~N z;25t3ngsS)km6&n7MjvzH3xY3CFgzePx^u!4d5vv8$vzF``U{V03fOwHR)=#aW$W?)OmUacT3k}cAUYp-Pz~OV*mSR*6Wo^ za41vvv77^JfFCx0{`cdu8URP4fyr?{x^jj5=Z^k)(v3s^FFkn+-8)0OqQ<{o4aO{Z z(G)R+wHy=`7XMs$xdm(ykqlP(^Hn5$KeX(*sfH8u8FcMY) z{p+|{qcj-M_an|r;}54H>+s*zxoV&T=nU`}twK^|qDru=X8!OPW{`X=Qjtw<<%Iv& zSC0$`K{2!4LPo*uJV-(|XLA07H{eyfpJ)-}YD52AhaBR|UW{ZGgt7>N3t84d% zy8tXopw!*ML;2^{d(cROG_i+afH|N>-h~D{4EqCIz*Q9KWszjTANm(H>p?|B`vm~6}5V!LH!C+yrTM@!A9HUIz-jFs@C2|n!ov5NgQJZn# zIW&LghI%iH8aK!ZF~YBJxDJ2vtmj=lyXl_k$=&=US#)dQsY}92(h|FKv?<2<*I3`#x~Fr%ADCTgguF zvA<}W$IcA&-IML^coed_j*k>CoK&s+LMCPV1{ZXBS&TU@zz;vR$`R=D4l73;G2UXK zs3@@Z&1TO|%IqX#iq>o0UvrobqWwRoo}ugs0wPk078r^L+)(AQZNdj52u=?Ep$9s9 zs^k#a;%Fl3mpoC1kBY&f)1U)saVIrySfSSo2qNl@z!qWV^XGo98(s2Fe9B}l1jmy& zVuIhJE3I5n(ZibRxs$zK8vwDARf02co7Le5+dxF76-ot{p38%h&H8_yoNXtC|7V53 zqEPII=&RgQb){ zF)J%N0HTJ~r3IpY+|xT83}&3c?D)(80R$^3}# zkTBfNbv79#kRHttBFKD;za`|Fk&yKfK3~k_?dj3#Q4wEdZL?&UCwv?*;4Xb5joWt| zSv>9NWEv1a`i=9-NTgXuPh@hYe`7q{5Gmp5LlRrAkwI4y1erh+ zotm!{>3%}GDlA`$?Yo<6E>{aHUf)uiq)!CI_jKKf7S{M0zQPSR60>S%W+-+uf*>S>^v-U z$VQJhw0hPX0FPhO!Doj+3-ObfE1~oK%=MfXsSI`>if~Xy*u#T=aBHhDgWNPf4V7Rz zFlxJdT|27YalE_BMo^~IdvcM^e_Vz6rJiYn`@-DX8j;%JV4 zn+CdI^Ez38?Wb+A?l&-g&vy7n@zn6~XZ_RIpABXQx~du^4%|X?VQ-6s@OV<7%kiop z#3H#GwM?^GQTWF%A6Hrd+Vauw8-ZF+V-R>Tw#q{!@OJYPMVY2PH<;wQZ+Ug5ji9$T z>}=^%N6&lTb^Bjl^m7`R6k=@TZ8$^HEA63%Eirsi!=l(rnk2o}eHl(qxFXC}tJinV zw4%2UsDR#0B+!i+?dbgx{lSj)wElN@f@IW#0@z`Pz{9P711nSRp=pL8RJeV>yCRm%Sn0lZ-Ku-ys7y}#7zr?u;2d2F&pAXlh>YW6t z!&Vjer&C~J`}dpUD?FePNGVPIY>{*aptw=+B&>K*m@ZQPP#n?aMfzu1^^~Pg={s&w zO6CdWmF1-+T?Ox~xWNF^w>|zx_lvTtj zp%oPOnSAMUP&7$TZ4G*?ERL+T%!|Ey9&GA9L}EAjZw%dq6J$3}pe;l6FX_HvW#s=< zV@()gbMy>AWsTEwrs{c-1l0%XQdD!gka;h<54+qY;_Y}O_mR3w-6`6rNquoW<{at7 zDj>~+eg{CLw)#6Ce<#NXz7Ao0H&OcZL6^_b{|UYQc1!=^X3~(Rz$!QIO_auHO){*y;i&_V7S-$8Z%T)qq_} z^m=}IT=u&UEL9eY>yop69n_3n{rY<>xnrJ}74Vedg*O|VWN*sG(EzZ_NB&kvC)~fQ zp@&a&!712CzL%oZHrJ^r(nN0S`R5gAb z$8fVA8WiahEy6|J+s&n=Bzh;v974zv$)Fk;0MHON69x~@x=9qzhQ+C7UCa3KW8jDHy-H zG%BkiffqFWq1Hfak1`f;;NXV(FS#s?IN!(R+NsD4*|{5kO`g=mbZx(RrG)rtVZc~; zUtZX?#5) z%Y|1vUES5%1aH*Z+oRc?Mk({&2Fs^d=2aiJs_E9r_&#v4I$`Q$DSMGwU%nfcTr|~IS z-FT0q#!Un%7X-tOPhJT`sqNOQu6PIzax`uDxVxf3Mwm8oFz%0E-PneXZ)0YtCX$U^ zmmngL3TDilMjiP3EMFQ#B&}1sqC=dRi69#W=eN|yd@2gw^~;GuK1PUPzBB{NIHoj% zL#_wsovY0cJYG94o$D>VO%HLuFyq6)Og=v)fM3}p8a7WqXXB}L!3o$>aex|dn3?7B z)li4hAqXgc{0wM1gwOVDe?(8dL%&AyO3Qn;EJEXjKvdJ<|_RfXo zE+e4nQWbC+=`v`S?s?5HG|SD8J3EA(Fe@C@cmr>e9#50byL5!oQX(4fYekdpdD->aKmJ}j-s5f4i@C> z{Ky6z-I#v%=e6wPQH5P;36^TAag_~CFWv-^6WK9=H6-`)>dC%$VcXPu^}I}tkGw%g z7s)Q2`?&xSJL&MSCgL+^UJ3H@Y$v%VQLnG-zOs(=7Q=SN`{o1mJu{%J6#=NzmLtyO z*(1j;LXulN`en|DM235bf$nOylGhoT_p(d8g>R&6Y>FjpZ3*|%1=&loL%Ux2L^?&x zZoc>QQz{OvyF@2&7+txwS}^SI{-JmxytFvey~eK;D!(sXIt|IZ#yA&E;dAe@6{V6b z=M{R8TNBvv(jV8Il(<@ku9dymu}>uSPL|Sey7$ww8La0Utatb-P9)_zN+^qR!s7OF z;6ns7ek#M$T4`{kt$ex*hXDJ-FE%Ss{6yglr0i*MySE?O7{t7AX99OP*cP80=0fb2`fAMU4HzXJn@bf3NVkY#W}`k_SV@-up=}ZMm&jh2EkKQ*j<>R27(e8kFm2A>o~1 zHyv7ROBly*m-!64d|?PRm4JMpF(&Givi0UnjJ=($P}qEEJYZ3qZp8sC7Po;Qvy3vdVDURZfJQQ!V>f}bU$%PJ?>~$geP~&kB+QWD-OT&G_e*KIn-uRji>0Sd_}hm?&SnrUkh4>(4%(j z)y0Fu;(K_MME2-rX?!Xrta|Ld~ zDsOfn3s${1ofHu}k;#)v&OWv#tMXWVyYsY_ic~|YlVo%Q{oP-UpJ!M3h{guk7MUdK-nxs4jX}YNnNUq1UPaGY6`*MS5Tm+ka_uDMPz%`cC~G1xwQS*2ds zk-z29n@W`7cofyYL=tOr2shT4{e;^$0AU4L(3x88ACy0%{lIYLhnaAChpUCUu;JAy zD_(fa5=S8_fOsWdJ8wNnc{%T}*yhKVEF`w>At4l_KZz~lqOnN2frkgcL-@mBYZ}Uq zx{_3Tlt|k&Ex&(tz02y_pS=w+0rZ$CEv$Wjz+=)l^!rziRk0=p(EzWQxLQO4J+#;m z=o6`F0Vj?Rlevu)?RyVvlr;FdlN-46oSTW0HRfKvVPv1ST^9#!Vw!u87mz~yAn3R7 zPgAiv72P#OX(7NzZA*%l$?wQpx;f8;wT_pihDPr})f5rt?V+MO9jBO>~@1#vcM42(=DsioK$3HFk z4VE27GeF0B-u>!bShHL8PkI^v7D{F*B`|o22GxKMb(3}N@)CTbcBMWdo0TA0Nv0vM z?lVNHJv!Q?;B#>C#Gbm~`Zg1}Y)`zKdobIO!|EIQnv3MFho-Al$|1lJ1)gT#Lw)5e z^jQUqR~bYcH|C!rI)w16eY875fme&W%X~@3RF8@`CU}qvyARjaDbIilzAt=TJo*7x zel~{MpnDem9l~iO-jKct$1-At+j#rP``_5wS=QKtNz~-B8PTV1AumQYeHa{fAjgqp z`GAlWnp1a*0s5$&U*v@K`1nXbLL%v>iT)j=$RCin{9;E(?sUb0!#-Pz=jHgQK{k31%^XI(ADilZ}3G+L!Kzs7_OkAiL5_b?NyJP%xTne+{HprFa0$R zJhly&9Wcue+q?z)|1Na`#1Td^0FMedm2!@UxG7Ts~E1* z60Yc;N9!dKeDq8QXP;oznP*_joZhvf@Hl+0c@)0N7yaWB^6HkZY9wD<5i1NR=Lg*d zi?APFXuwkMj^=MS#w)1uzX4AK360EL`lyhMiwlzykPo{iz@}U$I64$J3gjrntEbKC zR5dZk>5axm-q|C^HlqHt`8iL&PI^lq*s~r}!4}?P0cT@%4{ZVexrWuf9eyBGXDw1V z26fpV@;nyz*ubyxQIiBCHD_YGS6oTJEa}|TowVC>I-iCIW9*fZ2~`JhEW4p7LWs&c zfA;MCGX-f0Eilv1+wt64Q=kd+(~z_bqk;c}vG}vWA$MgWGPf^|$sYt67+57rU8S)G zrRu5%wZEDUC_nbS|Jl4~9XFbX zr>TVn$b21#z;2}LFx5g=7$%-UXg82F=ba}}U+wU!(1%pkom(rvz6up538&jfn1cgB z#)DDMZ(ue0N7`W|(g}wpGjwQ=onH`PWa}%%RQS6s?sNuG8K>U??y85#u$$i_1TuOe+&|N!yY8 z4E<25QZvt&$royDkqyAi*Po$J)7nsNvpXoe^J-xw{z7QcIS9p~gDUj6MP}0TycS7K z&zAI(!v2Y1y()$$(pSZ{o=c)(nl`1NYaDD&-n>t2iq_gSKU)l*o{Bf7GBE#hqH7|O zr0N5PR);u=XP_D+_GmICpc6P{zs5XYlb5|4WZ&HSjGPE;k=ZFncIT8M+zZq8FP_+a zgwCp$-Si&3kd$VlP*hc`EVh$$4GuZb&&@~2?qdtQMngRQtyyVQd5QYseu<+t-G@Pc zKhI{jH`7W9OX940{F1!cfk92J^St+kJ%P=IN~W67LiAjtMQyEziJ=^I+?nMvymZy8 z>sN?_p%0|cS5D$}3UQwUksH=HL%>Dm_XuazN|jFMFN?Cs{5W_xpVbVJ(?Rj?Gn}s> zxS(8HRGF=M{;QUod#J z@DTMAQ&U3gXB>AfWVzp1z+Yb{89xaHKF-f>+`oP}nLa)hdpf*sqHOxKFTu1 zaF*ZL5|?Z69^!#^&1O~zdicnIXh58yFwLe8D^ckA7#g~XcOVx_DMGm$1RS0B+mV}_ z8>rvWqctxOhK8uw7QW@-NAGcPPbaT^Td+US6Y0LNfq572F#a5eima)EUnoA4&&GXKADP>y1|LMyMIs5DI zJorI)3)wD1kG;JTFS~AQF#2HobUeLJH6%I-&SsZ9-uJJVzHK)8|4$_~3N9-|)TR(} z`mm~ILvDyJnkfw?*<&okq-j_}vXo*pYE$e>IW3zFpi!#=HH)fDO0~Wz4nO?-+2=lW~a8`u4*FLq^KkMFss9x#Y& ze}2{Ol_=6Tl_NN97~WtTE7fR_KbD6O5{P6(Mt3zsPMw|$7Yl-_` zFU;0ZRt0foT#5ri0Ix(j5BjjEPg?}lGG?BA!Kx*nWBwNk(s^7`N(WN4KRWNRYvGANE zZ|zoxV8OGqrJ{SpNx^(l<%rLb`iHTo_`uwk%69V()bfuqJLv}DMlHcVre0pKWUwAG z;_;uE2HNo#Oj71IQ{Y}2mQs4GSM5#|9bm_`WKxER?u-N>mRg~SwJ`;c?BmB}j5)|k zSRphyHL}*L4N6z_6;odleCGHw0nTKm1n#Bq{Y#}k2>nG_%2*;UY?eCt%BRiA3RI(W zUe{&Ofi`O=_Nr$5wNC|@98C;H85(P+OF4jP>RH<+OL;PT5TJe4Y^)N@&AQbFdXRQ+ zK`?du=D7rK#2yUnuXQ| zJQDQ{J~%kQGRyfT_4YV~B4wwQ8>e?K1{(PR;iA;)WhuM}9#Fd^62rw#>n=CI?`^)s z=t0;czFQF#7BB|okeI88K>L41<;s0Dj>wI|JAt`&jJglS54tWWs7iea-W9w8oi>MWavvQTbpN#P0w7a)D3}4mc^r5cH{z9 zJ^lPlX+w$b+$-ZGO>mjKe{`Oqi*z@;1*S0*REg{nkB6E#SDRSCC`FT>sz{v~*XB?Y zaao!@MOJ7W>jVU08>kO}j!rGZc%Vi6MBC6cB`gJy-yzY4H2{ZY;;MWC(usSeBr*M>=A4)weDbc)TRygy04rWtaxBgl8}J^u-mLy2R-iNV}k-7LAqvUmYb=Id(~w>1rt0(X7c-VdW3)4 zI1^ijcs)@(O~J;DXqRO}3WMGBotNb@6y7#YFMYmQigo9QZS)ty_^(X&K1-kO=D~eq z!(sQJxcZC~d{uHn9^sz7sM)a2JHmE|*=A%D>fXF+8(Ea<;tr&mu?YO8gryb^W)MpI zA4Dez2Apj~mBr<(-VW&wAAK9+_|rl*fTtuqOfAw)_X+tm{P~wz%HQD@)C3a=kt_QI zjH1{^Z{-3>Rg5=F^|;0h1&HO}t47oH@2rp_i?s%WUoob1T*WDdU4EPRf~dYE3-xP_Po0}u4sEp{&^GdU3bTV%F z;SLTmA}?smNFWet0eVhd-Tedr`8BbpE9{#h*dFW+AGvhNW~?(4jtI)%;9 zO@;d#CfiIh8Vz8i@B6KwwQ3=Tos84Ti|=f`LuTN^ z9LFgYaL)1oggdkAKGN03F2EEBcq^MJ^7_=K0>g@SGmbmjy60h5Stds2bSay}9Y>7^ zh|b<@Z;z6s2(Dv3%=YR;&zgpU<#%y^FXs#(`#Tn~hZyr)&Dl>t%6_{!;{=H1&1m2LxJD zbXMPd3KFKpnY#XKy{)luj0J}pL8|uE$go+NuTLEl32Q+rs&N>mU^&GWyQb{6a zYR(`(jJ}Eq-tH!wD zHtd0F>`B!)|GDX-RrYR9Y=JA&02#RB900;#%F|84asN^!)5v6-^<^;*zH z>Ye{K`i2YILf3PH_94q2q~OJ(YY)A~{Ee!V^>hBgTT9U*7T~_0yI{GTV5GEW%fx(U zoGTzbnDa_0(zW}P%M9cg@3TPFqP){})Vg<)(ZnUl4OL5N^2&Yuv_ zu-8yUjzypJoyk334Z`B)sw};{Cw%9-05$}(Oi8);!0QPnr|Qplfs;Y{^aa?pb*)Hs zdZZTi7Fa@f6%>J<^oj|Y3yQsYjZSi+Y}j;urUUZ)PmvVF4&No$6J+^i=?5Tl^SZsm z2lmcOHA&{&V_#J+p!Smvueo2 zj)rOKYDd(cW}8a-Yw+jza_5E*t&&@Z8$D6`w0z@^OO6kkLepn{twQ1`U@#}>&k4Rp z=nn?|O*QG`>xB{a|HIW=$F&i)UAryr5D0F?U5mTBQ{16gad&8Og1bWrPAP81EkJ?d z?o!;{;iS*^J@0wX`8)YdW+r>~?0w&BT`NHdG~nl&6KNms%u3>1z}H6bbb7D&3LPsp zb~Zk&V#b~*osT1XP}~CcYpIXmmz7k9E2X1vww^GnKV#`xScAh2b&Lzcs++g?~Ma8LWj43tpUEL(RA!5icPhpA&9uwBn)kK2%kQAq6*$H zROu0#4ZY@Rg<|SIz<56_uba=|0bt$J84KSpmEbir^L+m~ z__c3a$9b9OdpZ4}nKz~$CV3BDlKYpx{CZxZ!IQ`FlkcVRPU8WA3Vi#);_L9aDs&SC zEPXeK<<6vC--5gVQF=Q`N9(567VlSAR}+ug)YC4JfEvB;rNjnOy*R}kD0*2|aMam3^vJFD zLn^+Yv^3Af4x>M__ous5aGCV92erb}p_NMLtu*tsW6#Q_nD~Kyuub!|(ihqD6gEkx zg^n|!xw)jhtiFsNC;8hM`vVkAqSt;C{uXv}-d_!-Wr8LbV`!{VnMla}^3+v=7GCAI_x0%+#WsQh54-ejfdra;02b7kIF$R%n`Z9=*pD&yo7-c;q z5i#B##|a#M%hsDI10fM2DJjcEJ4R*$07;1aPdbbR2m90yB@`8$hcW(9d3oh8| z{>R<%`EOr6@^vxeew+Sj?LwSh;nQDUnwXo5`v#z5rtVlZ7WGea5t{u}4u{+LA>EF% zmZd(gvu8yO)m?a6qTM!Nfq=O$cguKmJoRSO?pMf9dH*0`WP>M+(0Ir*X2di5p`9~bsW>-Z%)lPK z*wlxlJGfSs``K)-+MVw^JKS@R28r{W#PZun*cK8Kmf~M7HAnSqMcRA5IK6j$9KAf3 zUP6lS2}5JdFKE(>zD_?NFj)a27|_jk(!vA~2oET0@iBY?;um%FS17OWpWPJTpP+LO z#iO{pbWd<|$uDtDrB%j*o0=ku(jWEyy)P&V{&&)L56_V)Soe^Jq7mU^I?7`(W{(GD zjcEM(b7@z^Zzh1M(+t?#t?qr}s_)cA)P){7E96Cgvb7NU_eF@O;O&}^?+n^aQOx04 zS?Bh-u%q9HH3!Dr~V>3#*}UvwM{!Osd}KHDZTt_p$vVIIc0uo? zBt{&_k8o_8Wi)jThMg1eJ+yt9?@Y5v1DMMk8XX=8`LSN;W~0o<1_hwC{`u(RN$nVs z=kU4YbKj<5N*K$&$A|0{r)Vlr>b(Q4VNeRfI>(FDE89YDm6(;8?5KC-!#Tp*3t>t+ zH^nt=IDHkMIS=oY;$X|542p+`Iri%-o$KQbLN9zA8=PQS25%kz$+ifij}u$lH2l)X z_O>hvZaFE6Q&Z%?h1eOl25`vZkFyR(Una^=>nLdBA(W(Vau4D`A_4`IstpHMnr4cr zo9I=h&mzqVgPlv>XCJ@7c!qQElZ5)~g5rf|ogQBfwlXTeLA!&1!D5#$1%R+!{xB*Etw=79{a=?5kMD zzz+9-ynr?)TlpGIyDA09lK=w4s z!nPC!(vH+OfpzUG+$vyjaoOXLa?655njYi?KqiE2ZS3yrVu_A?OeXk{DLU~33MIzI zXMGJtKU4n%ES)6+fFBgEPHq6iGh1v~2*&t_+(b5w=yiVC4O((a(;ErM?=S5*fs`~ISQa>3;{zP^ zrj!R#BX&p;hz9qMyrog*Ul>$E*N$-oeWwnHmqC-l?W{TZGpQdoe_N!aeX6sxSEuZ;!qB$kK9W6yG;LMQo_kF-T>c(yw&BY@a-$* z3!~2fQ^uaPd{!Lg)$qIxH1Fcei&5s(_TUJwX@-%6uPaPLhlTTl1o)}HG$^>Fitq)e zULAFZ19^MtlB6V7M%VtdOetY>?RC%Ix1mcKD3GjX)8;UaeDd5!br z$rx{mN7*C~xbUae(AzCkCq^b<7CV!#YpFc!GVv|v$mF}vXF}8g^;#C6N2Ky4!YKL@ zj}g@AV*)O{a{B4R4bho@A-BVKlP-G7h%b%WMo@<@Z*z7Sr)z;FA7zfA#i+nGqei)gAYKQ92e$+F#208n?xj4nSzjcFtZe z#{#0sU0D8($EPPW#PRe9hqf)l%u;O{=!lI;PHVo(=!klS@GYj9PzPgScnz9N?;Drh606wHzf?lpY3Qn0jOWuFql#XeWM*u-u@uH{`p(9y_ zo;tFVrE~Y^kSqLnE*lII-1?j#TH#>stnmRF&N8uX4>bvxf-fam=X8Bic?n2IO zk;~{dH3QAK=Nx8ek4Fd!Uri=HQbheu0n45!B-G-8nE7xVgpLvo;;5y^5Sg>`4Rci; zk$3Q*;WzmT6{Q35(otafHc85f-F8DVc48$0qKQE!l)t1R-VuI(+`m03GF0W#p(xEv z%rTXoj2U`gNivPAfN z)ip656(RSsUx;%T)y)cWjo!qVyeBVcEne17zJc zmVo}`r}rmnCgkvw83%*hf5k=#B+yLBs5tVMv1Lnnxhf|clT&$>D z3a`(+d}t4*Yr5io$rwkwM(St-_kY{gv|p%E>8eSN)enr@2f(e*j= zH4bCRM`*nsy&Zq^dU^`>J7RXw>p$F^x2Z~5ogm&_tjb=nK!Vjg^x--F=o+ktvwZ-S zb^OG&KRrba>Y^|&`9(=5lSkj;4}hE`2X7>IGP&2p_O`>IC4L+Ec30%c(T@03!w*3= z*C?}#m-zvq0I03OW^Y6&UU?Mp6T>$OQ;%9{q)F^o{OPvT5CLztcte+6GCiF4Jo)CJ zbz7OKpJyEHC$hVJ#Ow*a9|9|Mpv0!Agga!MewEYGJ!5t9l^R%HXkt}tq$-E}&= zxdT3Yk;Nu3oPKSQK-vfzx%3e9!btLORm++PO|2%K)>$2y_nRW|_a%5B&GLApq;;gK!L?TsIy>VM9&%&3rx^tvyGhY_LXOSSbDYGK*&(&PfJb%A8UyvdoOXpsvP1 zEWjCYV9wv#?)!iz<9EJMn!Qh=Cvbu2UwF%qCK+05ho1lT$hZ_!*5;yD@&%fpt*0~o>&v4kra*_u7VZoJHFYr zVCg-~7WMH;Us==ANJq^r5(6B02Xb3gU1%HL zul{Lj1^Roo@lL*41PzCS-y)}{e-s6yaAcz++_oL_!1Clgtn6rvX<;t>V#^X5r9~zGDpcx*-Hr&j}+6%r|16@5G;mBMI{Nl ze=URDrF&J`%($cv z7b=iP6i0eLPtNATo@zoXo3E?ip&PINWT}?_j|rg`YC?Dm!0AJK3I`;++4Wl6KZ>60 z>sZm&e8Eqr)r4|dJaCOjSte)S2`uTeH!)VVeg{IXb-V2bTg!`x6KZ3G&JX!8b(>E^8|s6R8BN^19_Kc{2_FE$GQB0nZ>q{r|z zTENw|##+=T?6H1x4@0V1-REd^4Pm;pHJ+n*yAq|cC+;1=hxAq;yIz#VMJHKcYbyuB zN^mNkBcHBXHZ(WLT!pfZH#XIV!8+}af?IZfv$&&;^_kZWEv|-$QsaNRj+QW9bE8BU z>nvg4_UMk<%Hg8MN@St~h(b33oOZs2inBk*;tPNLAeNM}smkCnr9C)uZpA7JT%#VH zit;>uNKW-q|Cy}?NoAdHw%6B4MNOPv&?6ClTVM_bMrB^dYVQDqap1+eG1cKug7P}E6)Jon>=Y+i zHsTw1cDAO|!A7wOXc!2{q{moDJ6|2dPe@1!rDll2yom04f>pZxT4bN26lo=es0%&A z-at;i*+K)lD(z`eE-3^NH(k1$(H+6ie>>;=hWEqnQ20U49bxKI=u~OO>z@_c1#J6K z^HJIrj>!=so$@ERdThm?L*#HAQWv)U*pS-ug(fu(BU~L|^WPO&hxN*hAB=qmF>O`! z=s)Ja^s*YQF@#S5rCs!{gT5p-R#sQ=V3>_-fVQ}|-+@q6o|ze>-D9uQe#Uh)d&Wu?Z4R!7j_LfjdS z;wPU$K2X&sK=eEx8iKRw$3p4{1TK%yrbd@&^V4=U)}?_5DCs~(&_`5uaRk%hyGR)j zm7c)8!-#j*C&cPY{kQk0L6_*-_8RIWgf65GH^TqkQ-R<* zAT)_Sd`5wp>k)v;5r*=4SD`0vIzoYNe)6rj_aWpPQvA*<(FzNHri~Yo(;8gj@$p?L5F2QJ$QV?$* zbQS)1(Dc1dmazqxZ?S!;(}T|mjBE}L&p9uLk@(AJDqB!;;e-dl4+=W^3HJWZgAdv9 zs7U3^GFS?``BHq>>FWWv{r7v#ZWU+z((;oFmQc)7)jTz;^sg`*wa$pCnb6J5*_NXf z#y+9B67e4rz-r8SO1dto7ki1eD$HB&GlDfAX#Xnw)_Y zB-@N3EG#hU#6R}Qnm%v@E)Rb$M-MoT*(^RUNaYvkYz_0vhksJbtjPTa13QsmJs`NG z*sU0I-if-PNCm*bRnx}nvSsIPHjn`)eJC48h1g~o83hwvHmGG zW>ksmPK8Qt`0a-?R`{vDnS6Izk57Bj+@_8hfk2kJF`G$P7zdYQ(#a`-d)4+q$R{qw zYKN(x@1M^dyQYhqzmI~d@8VG44wkWHfhDHwp2z9+o1KrL>cm5fR*APEtd(}jF zLfJx$;l+67=+uf8R=Bu$# z5X{@@H0~n31?_|+12LaLza3|cy79Mv0rS_%GiQne=ME-tIdjf`Fn4Y7w_cMz^-8fo zWJTTNSr4^0E-PNXZMzT@X+uR&#iwL}`2u6+?XXUbPq3pKe$UK3bxb|eLj4jT zhJ)~vw@%C*(i!D}^JCKI>M$)W8>19RdJ1aSJZwCBrdlAT(J6;<9zyWtr9|JZOPkib zItEEqS@y!R4?3IA7!()%El~L4LB&Kp9D1Kb;65wXO%7-tswzQh-1T^9LD1}}E{!|n zgX)>EV7>U%_y3v8zU6Npjs4Ti1O=5b|1VEhK-417xP~GJ&71;oIQ@SB24}F0`CHzn zIJM0EqXEGZx@?7R!SVn4qv+D~BQvXOX3}*ZnrCu@|Lejf8GDL^a%oYa^e5nf zhW!lXs9LL9P4Qw;aK@{GH2WRXf6)ZcqcKA!cLFIMU1}i-C9U_Q^ zVy){9aP-?apdzbmY$>;{x$`N>-PNu0Jn`sxu`R@N54$9kTlW9`<_hU^YP)n!+@4v%D znU+Vx304dJ`=9>vy>?aTwN9-b6KJ4v)MM1|EEeG$XS}mwA7QU&bW{8vcuRm3&tx35 zTBwuvJy`e9U{pKTbW8Vf#1#6_Anm68x9>dmq%caem@;R+%UW>Fl%~=SO;tT$%eKGk zj4~%eB(fGDFr8@OMCJRxUFyA;z4qJQNCr@5F%;ToMN`6NS%sqnr)Wf#lJ*bU$6xFR zX$u_~JaF&bbe1Z(NPkrR@90?z8q)%5Dl$Nc5273kSp&Tz-t;#iBP%fPi zj9=bF%l}JZ-E<=p&}{U93K`O>#Qwor2^4kmO(k^sGvi+VF!!G&qr%7jFEWhC9+V7& zWHkrP{DhudryBGeQ!}E>AH~WsIqI>PAeT??{=Z)@_-LtEr!LNH6d=)h2p6VYRY=J@ zS7$%k7>sOoi%km}r|RDO|NTM%hZfGs0s&Kt15#8Hqf+G6e+)y%|Yv_x=ykCSv?ow0U`I2J~5J%=o1|kQ50~(h528Da5)D>dx{$H0k^fmd0xc8r% zQOga=Wa9|yhyi^rHsNqf|1KvDMdT{}99#+bucX0kE$0Wlm9}}4HirT9TveGp3ch!J zLi)&yLPW4D(-U}rS|l>C2iUj@4$uGZW#(Y!LvIJ?9WshR5|oazLoIj<9Xi@5|9sBt zO4QR98sDC)3NALl_wEwnqsb_yDF3y zQ7$KX7p+F;_o_?R&6^&Ho*ZL0)-2hgyf5Q$wwNRs);U5NU2Gf0J3GggR}J1>v8LUa z>2xZKp8%9+u04MGxt~(3!<|`yUq^OiR%DuMQF5`R* znuU{F-o7lm;w7PCk(#lTlFo6DFL^P*(%z@+5^0V6HeM|dM61iML!uKdtFY2qW{;n` z7ep(bv>WH`Py<){I{mtKQNILL*SSGydB8ekPx~ZNzf4ygOb~Rof0>FoTVetv@*fTST&u(8a zy>jHTAvReaYGmM@m4K0bmJ$a_QLmnt6hKagaY1$4xp|+D=;!A*$){LLURIrsjds7$ zux#3R+GkbYw{00~Okg2=v%gQ4Y)CGq>Vc=BF`bZ>7M?zylz7fZSf2QX%9OQvKD;eW z{BVOy)qq<(q@*gXZ$(C=@3GLJE8pR7pqKcU3EAMoR4qw_(NO0jZpj~JN3M?cx(4hk zh4!`jdeG=7CkcK3xA_wx2D&7f+UnBxEFs%e*mHp;U>qO|ww)aC%D|OGPeZrbi zPnkc@72&ZC8h#2c6CBz`(DEwEtsncN3p}FN98bEN9>^ddvf?3XoO*eJ%>+J#5LU0o zv@0n;BjlsuIVAddF>iHfXQKxa;dc>_v%N){)f>3Rlgk7M?p8)z#zng08P@}7lr50x z*8taNg*hp)GhDSX>>AS=YI9)GMz;$!aeS6|aX#B++P}6)=m+C!jtknWIPp`^0)4;~ zvmoAiYi)f8hEX)$BH_W%(6D&4Bi61_wr_&sQuACAOh%>4)Fqdkz;bStjRF1#hy2w8 zz$?Zn+g7xwXy}dF^A{fpf~Hgv_nYw{xGUE-1h0C(PZD2I@EAK`k~-rwT#uOCmGXph ziz+k$d~}BB8UP&d{tMB=f2H>4%!zUKH=HV~0fa^9-Va;+WHi)?uDPJ&c1mVP zU6;Q+TRzwh_>d!qWu}S0lXdx^!6;)i`N}lp6p{%!%R9fZt>H82v*LM)f8(J1+O5E?ZE*(bM?T?Gvn{&>3&}% z*5xRXg{Y?FqP%2x6OxIowriEew&$X^MnHfm)m@asCxXJ0SY4_es++6v$@*K~23XM$ zH(bbQfW(0&^J=>!Nmx#$9(0!}PdCf4ZidHaMU$qVxQ=LZzmBMGpLf=G_I4B`aRyTK zV8t`eMs6hSs3l&=k0ncK3!s>hg`v3Wj6O z{4>;Y4xnX0NhB5-h`c5I(n@DpxLspM%$9s*Dhl!4cF|Wi$U}6Hr1G)W1UdxZa_#7+ zzFW>Y=#Cv^ka#ZU;%)9_ug4485D!%i(nPOljJ0vFleV_QsbI7-tDU^W83;8-H5yMj zCi~(8wj4cZkkq-x!apl9&yGa$J^soQ460E+$W}L;{i=K=LgMLKy&Ct&lFHT9&9rWa z_%ru==Ge%GB`|R?9i?kI{aBmNd$+bkl{loQgIWrCeQwILMc7e$Qlvd$LA#+pos6s50U9dbnJVJeAi09#%kXYhh0{-l?4{dkw&52KG=5JAnN`Z|{7GxQ8`q$1U-<+t0LxI$J^1~XQhWUJ84 zJsi=LsSTHgC7vIa$-$ePRj0j2?1k*5aAO2%bP5xa!`vO6w3uDbvnFb*Goag3GCo+d z&=9ir=L^=7u(}V%-hc0^rWFG>N~ z(h|NUJxLZp{X)uxpd*!SQnZ!6wa3SPC?GKy8PTC$(q>dn`X$2<7&i3I9>Nz_Rpm&K zJ`aDLqW`B5FOeQFQoQ>p$ zc~>5!Bh7voWpcA|u%+6mk7`iub5`s%^4vr5jlFNKmR=|E%pe4l7M#b31nKWi@!#70oS$rBL!SNcyqHJW|c#JYaO zoV=!?y8Xv$aAy-9X%U;PgM*}p4}U8iZ`kc=MoO;e%K?Tb44qkVIBnKogOdFi#s>M@ z)f@UxXx?pv=02SY8W$zS6AHGINPqVXC1HE?$=2%_&-KmX5W`6*FR;)M1#ZaHIAS+V z+a@~c;z+eqGQa{gi^!vfuU(hOrZ?UvVA5_6&4_;T^X4E^y7Nwi-NYwu3s*-}TzM~! z*1D);r_P>8{=l~YGU)O!txBZFKMJ}C*Na;GEJzz(E@o8)Jmw3a7pBGS(b#eO%A(4tiVFS@6sy(E)9JwI+AC*(s~25_eGM zwq>qhZ#1XP)j~a}6oiu3BMl_o$d(>U!Ww3I@xTGzc(R{>cG%mXt#cP7?s!(oAKB?q zQ}vPNgReZUsCA}m^~gRW3#&PUBYT8J^=UerB5dFz8 zt~niCyDryDVgS^PlS4E={?W4+^oadC{%qUF<)dwadC=*UZS=%P9@Wsw8SFz@Wj!*h z`JFUD2JL#}SdWH_DhXxf*aX>Bt=SC2rQSpdAZnUNykM3JYiVrc_w|C27P_4D=&cWF zB@yS(wwz?5ftBX&BhftsU_LU6d~}*jqGR9nq}xp_B8@X}psBj6P!qjpW~s}~6n6}Z zeZm`+NsU~V(=g`aQ1(|?c|z}Odb~c|@YMwnlnqaAF^fl?+k^?b zs$9m*Hn7+As80hja7|JdH0xPAjWcxtEUbpvmSLNSlxaq7eVq@koUztjT@(Ig+)Wt? zz7N@AU%GVMzTB~s=%l6*=_U_+!YzU05TD|tLXS=Mqjr$ukRPd1F$f>VfF0ds;nD(r zFIpxD$kPBC;6+?eH>t*~lQt{)c)^sUK@E&SV zJx?DO84T&CreO67Y>y$wIa0-k7=L7ax$Ek)*2%X}-C`{OLL8SdKv&$KCw|c(p-I~D zCow(A=VFa_DFYEP=MrD88EAKv7Ptr#^OKR27in{#R}*4s*m?=~6qHOa(le3rt?LaU zn^E6VxcLcW9`4{qgdg}O>MT1A+w6wv5bGh4osi>?_~MpbVU3#L?l2k{wdqs)8X><* zTs8Ckk_D`*IBeV3?`Syb#V9VCHKOtTATPBsL_yu3WC`+T{TX{ig`?{ry`dG%oWTRB z8Nh}ZFAGc!51Xy(%uqz@-B%Q#M5jP)L=~s_l{x1b^oRWqFGaeT63+T~7bg(Anxt<|%<9b8;cuq+gz={z+A7gR1HEv=A6Q73D(_Ri zTf&(H-jLjWFRDuDv2bp~#Kd;p)VPjT=q++EwDAw`dV=ifrZ?ERsw?$6P@ODpp$&8U z46&8>w%Z!qZ|P_H<6Dq}>8%KdX_w?*K*h^~)6jw_w5DynRGRH;>Gs+c5hs<9ZZGObVMFa25D6;5d zPdp^|M>}fkM2{4kGn$=`E3~IaoDg<8+9klr3m?&iu1jxUYJtN-6&<=tk;|9`^X+?> ze-?{qkNLc!pcG=eE_pc_UXldW2zVfJv%H+suUOmfTK?U$D*S2vu8-QEi(HWChg#K1 zWZ+m#*q~*Et?m0OH<|X9Abipe1ZHMt^Fhz=O~444n#J4xYutseyMyasJ5Lk;v|bIR zZ9o5K&|fDp_k!9bIijc$&M6F*v|?RQ4Taj@v#)medV0mq11&0^s@<_|Ca-LNXV4W4 z(afXGK>^mJe`brlvz`g!$!MUWrH@Hgs|zb$6?w`W%6A*-Z{yYMtwBj8)&K~UmiC~E z?Xp)&o@h`Q!nQGZ+lq`@ui5ef(MqLv{G%+hm(?2(9Jc@N-*wM(>+;v#5OTM-gN$lGHB*$S^3 z6MX31!{*{=yC}6|tL@waKE6w($AFh#Hj~&5$+n^MsQn8GD8jXy6#d-2@(lAO>K$oL zX^6~?pOpGDjO0~YqJ=oxZ>bCBz?XQ@1TJ1v%Mc?Ri3nQsgB=F~jg;x0p7*2G5b|p^ z7-M`+Xpmh#ia?k(>4JlV!RM^({tP4UxZWrZ8u6BOJGcegQb7To0dg*9Y+4p@4^^1_xuW~p+y#SxGbg)2G zZXX!ZvAl!<)NuO{C*Zdv@VPT+M{#2e{pAMEr@By==!PmQ|b4&x{a z(6ZC%&W|S@sN08n`oVgMyW(iPwrXk`v9I4KC`4deF^Y=XyJx~zwn$N-ea>dKz*xa% zCvb}`{Sw=-G>1GYXTrEj0LIG@b%`uuV&)HMnDZC$0k38Xr>i6R<){ zN^}#yLIGx_t0pPL(afNQX{l;Q!x^#axvMHJLCft3$;iwMuhNU&Er6e`CusPn*zSVE zmV72wcoMB&aQp+o-;*16d-~%-Lu4$bLyL@^=DD;v6LV3$-OQK7d|yoX9V1Tl=V6Q9 z6T2vN#f`N`wA0nDu&hU4p%3V{t|aR1QwG7Rtk02jQk7FttkKE7?nmwujoRC z7q586o&KSllgLV{s(m&&fI!(tLFczgw@mty9iDyR?%^pRD{E^l0%e0<8~q0ig>H+m zCO*s?2LYwThD?IPZ?uA#w5SfIf5ChkZWfb#3S*@OMf#|H1nA2tSXG&sgx4O;U=Rs; zCRgw7Evk8RUZGMP;lz|$blsFjN}3M1STiDu;UdSyF$#&Wkv%FrG3DEn4qIDsey6lHBzA4hz5u2_%N3GGyK?#prm8qk3o=BQ02v#cLtM=9#f z4Z7|+F8rQYJRUr5V@4i_`FeSoqO7=518?#rE1B2XPs-8;WA1NKzw>I*^Pe9SsXIm^ zgc&uY)%|w@MHsT(YW&Khb^hATPEv%spJXgFB;M}g?xOq7#Alo{UUDn)A1E5!!(awJ zW9xh@@5YOtd`!3rjYRC@JD6$Ka^&4iz$8uOmt%7bk9IfZA15nps6|8+Nm7yOA9T&P z8CH@C+yh$KbP~t1k4^aJ^r%Vlyt+2iGJ-lB1$SdKGqOk|B|Qig6y(;XpVQThREJR+ zWbtH~C&^?=cL54N^!qt^IAYr7Pt6X$>VY&1NY^R0l>PI4>foihANRjfDNU*7U$Jn7 zEK3MiwY(6XcF>DS){|AK>0-Ut4ERaUT8%dqk*Wntc%6!eXKvF=Enj_)llr$GE1HX0 z3b@{W1q?^NAnkyXC2lGypm8D3(?$f)C$rM2=V(Pz)(DVSVp}6S9AaUOyZCt`Nqa_W zSXPVz)R%F<8qD)0mkNAz{%&0K33tG2jTIE%ZuNYgPHyU&AG-WYIQsAL7m`ka-tft< zilQ{&rV_mE0h-WUsqP?;=tx`~E9;Vp)updlhhIM7_=Y2>CFPLl0H1_Z8fJl8rNmwZ z`J6p_9^cz&5xvN1``DW)@F7L2x>@^>WV-Q55gTCT!~1H|jz6hG43%{Aq3kQtmo^lg zPf`|hic?m)kXCnpBuTA;QH|&9pmv!le~j#4EyGch^gl6rg{|nhG(PpU7V+6#60Il9 zn>~%u_g7x=HAfhx0cJ8b(fWit=avN@fFi*EH<|LVH`C{v{wEoD7KG#^| zOyPy&tIHXEfA8t_iS5+EWuhr{AV##5NV!C&oOor>qHlH&hivImsAUtvs2P&-~RHv&=d zP@xz7J&$rpoP(NhdX3!ilBXli)BFWX<*w@ld;a`WE zD8qAdJAdD%de*&(iezg=WT#;OiYFOr^&zFrzF#}pQ7^)s!zwtE)gsA8wjMmHd?2)SRa22aHjsMSv}~Q7$O~5w`+dg&}l+dZOJNg|g?$D7Il(wdrL1 zot>L25vVTu;q~sxg$_1+bCtMn@a?1v3?o!aZB588#qMQpVREf#MbbgJ^Zcr;D z(#{@}v^3U23Lo}(O8a(>3vkJ%AMp*=hdt7TzS18sRVQYaLP*aj+#C5@T*G#kp;bID z^37y6HYA_>Me?9{lYmS3zz;ztzg(fo*_uewBGdXhzAi7mFg#5GTzk{Yx|=i8QI)#66ugl z?&|bm_2~Y>60l@roOH==wE_aw!&0*Qw#__{MyK)xwi*-}HgQk8lK*N_#>~#oLq$$D zMw{fJipf3T0e(6vgoP0YTH~nOV?n)DK(81_F+*h<4W6>)*yV3wnJx{ywv$tH@KgZ* zne>Z(UXhrc3&)km3q-9Bjii*xcjE=V?}%>sCkkctd^2PivV-NCE#`CXwOw?sA< zO~RY1NVU`17@I}=;#VT4WCqY`% zr!xSS)CG(G<1bw=8>7eLZPD*v`mv$7;ydIcLqSKwj>=(Gex36Vg~4c#GiRvD-&|#v zJZy*%)$Xr^6+(wX}r>{T52EHB4*R`xFDMZXl4zHbJJFa=Zn~sC^q`d6v zazqBeatikTLZSsJe4;_ns)< ziw1<*RC>O;(^mA+uzzccSwr-NM!}=rc_90Q^&6B54uB<7iT?%7& z+NY(u2MTpO%Ff)4BL`EYJ;C8_hws90-6?f{X#}?!hop%Y@=daaKS_1_z57WU%SGv1 zV6m1lw>h34fY}GuvuPOH(qx8oHb$;I`%Z+5`^34ryEFT@E;s%HhX~O$7&4tDO9(RO zGYXl-;Ha;xeq<0-Nhhc?@T%=>0zsr754-^-vs%YUA8%8~`9f{K5bQ`x)3dUQe0$DM z%T*;>pQc;-Rhcc<@kls@qTnLP0c#bZTa|K(164qkq;GxoK)D`CsL_v9)IA;TXk)~O z{B5mjr=vdv{)9rGdUbgfh;Mq=OQFhEbwdUK#B&7C_SqsD12pa+t0~Wu#O=`g7N<;A z72t+^(=iIB&g5TL^usx$nb%BTuXn2#CM`{QVQFnZF8{qY)DPM9V-5~$vdZ%5x-eiQ)3#ec?qSnHGw3Q0iKkW z{T)udtBUir_V|6(?)eGgQ|-b}Feo>pv0~^qE)9v|5z{!IW~*(4HFeL5Cw4AKngZ-A ztIcg~)g${rw&=U)IE3CRBP-VXI%%hA`dV4)uA2Gcf-Tq`bFJK{$}Zd_jvOqbZZ9p~ zy!d%?CSF0%xpGjr{6j0a2PlB_kwWGeW zDg8=%o_YU-4e_U%xfc+Ap?C_@vhf6%TIFx&^37_9+<^mPO;)!RiT>(Z8-% zCvU_&0;2`GYrE+Df2!{u$qQDDt$!a6*_@N`ciYa~5F~c4+L|jKFQB=}CUQ1^0IuK9 zzV+XiNa`+{1`P&JwKr%6fXcU)Fji&7f$S|39R&c1HMD~*sjC~9jfEi5eI zX$OHV^C5O#1>~YMwi|^$VhDc-=ewlW>vl#(ASoYgy)0M=<)kl4^VhT=fYVKb=4;qX zd12B^x0R*=(|3FpxPI6nOAm6U~#{jjhmrR>q&0gHPV=m$zBPgs`iqbqoAGA1(qi_VM z2Tpe#us!8RnLMf;tfQTZxC9S6_MF$eM+kXORVXR<`tl}a40|9)TFWA;kwJg^gpuIB ziG!8}OxyDR$?jt{#up8nj;IoWKG36Ws4^q1{x*gH^*S*~ z)siRY;|j%D+5nGk z=FP{ygU`+%zbwga3Wq=ozZnv>Reva$nq|r+rnaYcMtNcdKF{5u<}0R-`!PvTHq%h(r_*NfeF5;n)W3UvRBbb|}oCrnjkEMktirWE~%y12MV_t|pe zGG6Dm;I>igf4>t^fRd!@mG~v1L+A5PU(iQ`^RjXlpc5cMX`5qs)-_2FnFxxXe$;d?0By{+n0#2-jG%oRF9l zKK3fkVW<&KS4*y}sV=69?hXbgspQav>?dKn$FfRKOVfgE4S^ozEPH|}7!Hh=DYwMT zLhre#17h>^)Z3p8=kV#&w4rzq8M!==3jr~yWLpyq(TVfSp(bO+#3oeyiXk8n3ZcAw^Q0*L zVCj8ANdY;Faw>toS~EvM2+Lo~=3XY8WQvAetM8VyofC0k=?!M-tP*&&qHmnu`;L;{Ws&P!+Es4oTQ`6z)aX4gKOxuy4xn}tt_v>y>-B<&Z_e!CmDof>X9$BLy zXXWVQe$XAnG$v^QUARE?aM$qWeRSI9R@}+@kx;Q7cd%Y~aggFF!`Oe+NF#075RJHu zr+!W*cM5xEC3F8kDk~aLt7<}*11f3D6+3QQ)w|wT*c7YwuVy_;M6j5{^(JDX_2N^n zNLMfV{&y`(H_V_ZVOrm3HQ~uIOWr=li_{`J|0!<5?I`nG{{y%m-e5jsU9bs*3IH-= zU0=;fu2W^mzjXh}e6Gqb1yHExGe?UFQBCH>-~II~}LylS6~$ znpD6WsB#iD*?rZ3=X*^ZzW{|Cbxai_y37E+Wxl@jK0b+u0P?sX3OEy~Di8U$Kq~c% z(soC0R*8bT(g=}3xHTL=V zUH2AdkzSQ_p6AZ44h(VOuccekE6H}=5l{TWow%(tXfxUw_RVLKQ7P)}xLqwc<{u6gwgwl8L+PUM=o=Wa-4nY_-IV zNbqWJ=WP21O`Fw;B4Qv7IQq$EZLr-!Ssm>Y^T+#3bAk3!Bw~i)C{GybwO6@^(j8;p zmt`rAFh|Eep320z5MA(snQ?eY4T*7=Z}4AR%L|7x+9gsDnD6COl|AG3uVqANeYKV( z+}Q!RrM3VqghQ&<+n7+h>9PO778muVUL}q}nnSQOG&hi>tUcMPnv>)N&NHfhorYsI#0TaD4!w#~-LiqY6M8oRM=+i7fmtM~Ih zd++y~Kaw9=S(@{j*O+6R<2c(LoxQ_uNBu-M18$`aF*ge3yx)AVEC%O~1y@nOuhRIq z{pcg<`WkU7?O1UGVj!T&_iU&9Ghnq0OLj|B2$1i5g;Mqs=&F9>$_O1u3^!~{Uk~3G z9)o|Td4tS%&du?o>1TTT33cgnLsN9+NgxV_3H6D}GNHSr2HzsjzS)>hP4-oYI;W-{ zY;2d9xY_uA{%#UXNm=!K?{yeE-?{dD;(!~tcPO%2)|=(HGmDjz?&h7WP@1+~-DjKC zf3(E=47wl^10%esqobLa*24J)`I$QvyAuDVtkMCL(Tq%$&gRE-%2!Kt z>4uJ6JC0Relwue8+Hg_R)H7wRM_e7Vr3TBOSEVFC^mD@HJ%*?Nvk}+ZNhs38b~r7n z3iLKfowD`+^saI?wD*Dg)Dh$S4VoH(E(m`}qwa(?@8Y1UWVM1@?>{yBoXByagDaiC zz?HIzbvAQWF--i-p0zbz1H_vfI83}F(QmqFQgxE69_|ayv4>hyZJ}p}F;lt9haW?I zW2HP2P<*byIM2;BF}jZt$90kl?%Z|?{6daSp@-Y>EtI5ePnwx#NTy}gkjO~uz5N4X zHDO;Nlp>5omi_&u;olWSzff&R>3w|Sc>kPA$jXw192&tjsnfcc$5Pe;yP|&aByUoK zc7?=oSAcokl@-mG+iI*jo})t6`qf z22Y`|$;}2?t*S_9vDlYg=cn-`ir*JZYLM1Qgu-e8y>v$h-w{jI$O%Kk#A%wPLa7a) zr>|$BqEf8GR=YuJ+QaEQPaPJmUxjoQLrQ`gEGkyPR8B-UxEFXUT+EdCF$ssA{2&C( zhThQ^^U26m#^<5w4CIT%T-<0C{>NyLHHvF=RYy?tRUPv+ac!;1 zJJu>?1m_qHBYn2cURu!rar#xsWCsF9X`6V0HwW*@pl$B^A=7oBVfeIe222+MoKr-B zP9iSJQ&%T%MDRY^yjMxg;p#~7s6qcq8F%F8a;viMc8F(QC#S^LaqQ7_UJp*-R4V;+ z;XNt(<7|yaV{HxjhoywT+r@MX98*t2Yv(6o)su%gdUB zNq4ueg$aEesz!?s^9@lcf%?h4;?LDC+i7g51XdZY^m|S)P8x}mCdXT-=M?`@wf@II&_TW^$Pc#uC_(RUj>790Sp?-}_% zBgiNOZhe=btP{+V(4Pcg$M`Y)SFwBOXEq^XR`#zxojN4dt`=Ar%$f9m^y%(u|64{~ z1+MCWN->jK<@+>?Q#5|0-le=Vk20bS-R6!33tO_84uYHO)uw9xV`>W4nBRb<*VxMY z$`xzvflp{G1z@3kHKS90PSw(@_3>!k^8j!Hq!0eE%jv(Vpk zNS8eRO;LpURp&j{YgA=?taGjIk^ig2=O+aV!dawiojG`*e6Z?}Sxy<#`Dios2O${) zzr|iN2U}u}!-odoP(u(%)FWCj+HqTX(PVMP0eA=Nv{V5EY+n*rSd<`I5`7<$Rz}8)OjW`uV0$1 zvl6Ta89KP&z?K**1_F!>qlqCt8?L zoB1U@88NCFyv=r(qrgL{N5i@mY~dls5BRYAZN-vbn)}5z+f(k#zikcj;0O|2%4f

    uiUb&o|RV*|~c ztnac@&l8E_K*VQao0}@~Gdi8}*UikZ_KtY2k~jLigc!BVD-$0FzYL~W7#>hOFKlA( zJpsIAM^z*wBIAc#dfw8XDUJ-2NZq?_h%tzMoinfrRW?+F5Q4e$9$LyALCxd(GkqA` zmK&bHW+c>m7cm(|x~R#1`;fl26LUmYSIwpA^>xHFMKFJO_m=WtN=*7Ray*?i(a)I) z;S(i4H3`l4Ku#-jV3$VQ{gsoQC4CTP;ql`fNQXH(VZvp6xf<-7n52PEsXz_jl56up zloRre-CG^?Bq3jH;UZF>#k z>0ej@-DytDdYL|49>Xda6T^X@RgCB~Wp1yj@^FWZOsF(xJ={H}*dN1bXruIZ{+Mm1 zI)sw`Xsod4pO~DD>}VPq;g@04x4e)TsiO(-{!T=rkzhOIg~r!s&5nMZ^A>LT7LRRZ ze||0?>>~iow0WB4ySE^_U;O34~koHfk(ryv%dkn#i z?(~_$@!y+`(az1bK7MkpGtCjpWn@enlC|>Dpcp{(*fA3RjcJ#bl?C~!O#b`$+%HOk zUI%QI6bom&vEZWOfxOiW^|_)mZETbD8Y=+i8>eb04*wxOaW5hanP~6lX%KX`)lLKD zC`c}H+&Gsa8>wRy^9FwV@~w`Cb(Eg(b_S&1x##4jtiKBqy(H^LCJ-=+r#vfpx)Al# z(ehnJ!qn81*29N}b94#m=)UUFoVaT6K~=|xpx8fX)a3P=DyMm5#9q-3%MF5O#|^%8 zVV=2t>mPr2DnLqbNL$%pZtbmCPUf37x7QaZH0CRt7jqX>m;O;E?WK2XS0kb7z-HF0 zYry5%bMq5-gKreOkTPPpT*}_=5u(dM84%X_aMh&oV<0;Y#YEOQosCytqMzfHa`Ow(JQZ2$Z0AvS@EV^$i2Nwy@K)i z)A){$+D9~-P{t9P%iVQwzI!6MJbHkOcbf)S=31ytfFSh61dtf-EoUzUU z4tCtuMsBNXSBBa{ce_7f5A+^^#}at*?~uk%l2@nF@wtdS7S1nyPLeXkKIz2`@U%92 z46WJCyG-^UnE;Z-7vevocQ@aTxll}+E=s+KrZ3aN-$t_Gi{xWlH#d@wj*iQ!K~}!r z+mx9m-F*TQ@sebW>_C4kdvLMVP|T0)?7L`UIjo5^2P!~r_nw!%A1PPlHR)rsvyg_^4xZTNmzgvG}D!`}` zO*SiWv#>;Nn(CveFqeC@natYs$+R=&N3adhDE??Gj% z=A7f65|JeRF@AtC3nuv6)i8_N-Z%lrI}>_;u^SRYO5Rnneb zFj^+$veo(gq8zEYsL1rClQpvMD<(8#w?;n5DKMtV@x0PG}aAVqL@J7JE(lb}%v+NxmoxC5^?(jg`T{z?`b!He`_x1Ms zyOkNAYSO2!qH3YV?>iZEk=YBYizxJ}*!{|%6?nMri0(##9K&;( z(ozR+-}6hp*^aNu*Yd|X3r8MS3i|8igDE|O0Hdqp3Q%6BGu{_faaGAOfK1&u$%zX@ zR^;@q@NI=q51K$@M77>kVWMF))Kt5Ltgz3yUqd{&yA%aD`Z{R84ByL|O zT=FPUGx3od3vj)w?vx*)ckhlGo8EyT)DA5K)e>A-vnqpBSi;hOXzfUG%r!(>n#bJg zo_U!YU++Py+=^KP(HbfZ{va9%nhNvy-{aIfm^AM?CP5+TDL zmj*?;StL3M8hzP`Xy_4o($+k4Wp9vd7oIo2waDw4*iRC4W#Ysb{Uk7ia#H17W9wz( z-8lAYr*(RMuigwu7M(()%tb+Z6P(Ns}jlMoCFA8^$4 zY3)o`eCAzchglc}3WXSy7)y*gFPH&Gp8@b zmSK1Yd}nD;n?({f?Eu@r0LjOMZokm5Ev|R=+!>zr&J8mRf-UrvRa7i~Y2}R9wN9(fKtNsO(_A8bOA(YF5R;_Qdd<^I0 z!Sbs4-J8iRDXJBHx_TU8<54?dijOuexXXp6U}Mk6kw^#VuK)E9t1VxL%Azbqt>4G; zpGMQaTB$7@2a;G_FJbkNrLHLCdJPIF@A=AkA_ByDNvQx4iv(3q zZcH!{AS$|?gj)IxdvWpNp(1zS%T>T(1igwnPlZFiW4TXYAw=P9eUN3f9nAQMXl;^G z5q7^aaMbG@8@PcA+&C$TqO8^^K^>8({tQw(A!zOzJ(g}J8bKC3>)Uf+qXRPepW_-p z@koj3VfHv6lEeafkZKSaN-e#q7~|RG3zD(^V0Zk!DE%*97qgc__ZOc88jjwR2W;Nt z@wPgggu06=tyltWzgh{t*-Gk+uz4*->D zYN;c#Eg-_JLsy{;7d!a{uamj22#7&hz{R&9`gzH$CF$y+X8Y-iRXt+#_VqBF3Re-Rwyjr2p@{arzjEVafpahmnK_Pn|_(E6Z!#gPKSg0>1%b*1GT0~gh;rM41^Jk!o=ay;} zu=RRAcqAi57SA%WtQ$(8g#_cp%y3(0%aOR8|NiWd0UuB!*bmr?$CZ&(kCO7W0c+6N zK0Qt>z#!9l$3q1Z^WTFF{w?jg+|RM9Kj}oLn?L}drfq+2yo1c|8AAM@8REORd#1Db^)a8x)4@xL#Mj(FOI74?OF(K7~kw8Cup2%ZrOY(Oa< zt{2EiLMi_>rT<@3P`cDh(bokRN++q#5d$Y_hAmzdrV~<9W!w1&$_D=I>=P}No2n#L zC?${ILzj_{IPA;DgLDep7m_>|fLkcRH2-hPE0gWZE$hGH07-jr2?F3LSv%8?n_V_Ej|2NtO zmKHqrKTugWx{Oz64{cUYqz)n2k}_yw{C@NP-w@n`7uaAEP5Ptl>6Yo@jRX)x8AY&) zY~&8lOqfh}x@P=8=b6XzZ{~KQ^%bWcSM)FDmhtz%I*u)4d%=$E8wh@RKs$^`${>&b ziqMZ;brI?Kd!c0yWt9G%R;H99x^M@g^jXmNf>d08SiV*z@;sY=g^c#`yFi{n))Y zK=9*LV|PpUYDUp-d!U6uul=`(?0-g=J(PH%E7O!mSy%e{KR^-7;}qL=f0uZtFVp_L z8@M%txTEb0;gCUoV3Bj+61BzY><^Qe{3z?ttO%>Ve$Of2&W!XY?YzAjyK20-uQ zCMAaekgx8_OSU@r-#-m2&0avP(z(;v;uMwhSyR4IUW9(jf5Uw@MgDg9hMP?LEx;b= z(Gsa(=YkVZA5%TdKjFgH;#s(`OrEQFp?$eH-(yQ8{Os4Io1Yy!q3+G&e*hnCb$zvR I6`Sz?0TS?bS^xk5 literal 0 HcmV?d00001 From c5659f7d38683348aeeda8a8eb27caca7afe8d13 Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Sun, 17 Mar 2024 19:08:21 +0900 Subject: [PATCH 14/25] =?UTF-8?q?CustomUserDetails=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20CustomUser=EC=99=80=20?= =?UTF-8?q?=EA=B4=80=EA=B3=84=EC=9E=88=EB=8A=94=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EB=93=A4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 + .../common/authority/JwtTokenProvider.java | 22 +++-- .../server/common/dto/CustomUser.java | 22 ----- .../server/common/dto/CustomUserDetails.java | 80 +++++++++++++++++++ .../service/CustomUserDetailsService.java | 4 +- .../server/tree/service/TreeServiceImpl.java | 4 +- .../treeItem/service/TreeItemServiceImpl.java | 4 +- .../user/controller/HealthController.java | 13 +-- .../user/controller/UserController.java | 4 +- .../chukapoka/server/user/entity/User.java | 2 +- .../server/user/sevice/UserService.java | 10 +-- 11 files changed, 111 insertions(+), 56 deletions(-) delete mode 100644 src/main/java/com/chukapoka/server/common/dto/CustomUser.java create mode 100644 src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java diff --git a/build.gradle b/build.gradle index a94d31d..64dba38 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,8 @@ dependencies { // modelmapper implementation 'org.modelmapper:modelmapper:2.4.4' + // OAuth2 + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' } diff --git a/src/main/java/com/chukapoka/server/common/authority/JwtTokenProvider.java b/src/main/java/com/chukapoka/server/common/authority/JwtTokenProvider.java index d1f83d6..ed68b7d 100644 --- a/src/main/java/com/chukapoka/server/common/authority/JwtTokenProvider.java +++ b/src/main/java/com/chukapoka/server/common/authority/JwtTokenProvider.java @@ -1,9 +1,10 @@ package com.chukapoka.server.common.authority; - -import com.chukapoka.server.common.dto.CustomUser; +import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.common.dto.TokenDto; +import com.chukapoka.server.user.entity.User; +import com.chukapoka.server.user.repository.UserRepository; import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; @@ -21,6 +22,7 @@ import java.security.Key; import java.util.Date; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @@ -35,12 +37,14 @@ public class JwtTokenProvider { private static final long ACCESS_EXPIRATION_MILLISECONDS = 1000 * 60 * 30; // Refresh Token 만료 시간 상수 (7일) private static final long REFRESH_EXPIRATION_MILLISECONDS = 1000L * 60 * 60 * 24 * 7; + private UserRepository userRepository; private final Key key; // 비밀 키를 Base64 디코딩한 값으로 초기화 @Autowired - public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) { + public JwtTokenProvider(@Value("${jwt.secret}") String secretKey, UserRepository userRepository) { byte[] keyBytes = Decoders.BASE64.decode(secretKey); this.key = Keys.hmacShaKeyFor(keyBytes); + this.userRepository = userRepository; } /** @@ -63,7 +67,7 @@ public TokenDto createToken(Authentication authentication) { String accessToken = Jwts.builder() .setSubject(authentication.getName()) .claim(AUTHORITIES_KEY, authorities) // 권한 - .claim(USER_KEY, ((CustomUser) authentication.getPrincipal()).getUserId()) + .claim(USER_KEY, ((CustomUserDetails) authentication.getPrincipal()).getUserId()) .setIssuedAt(now) .setExpiration(accessTokenExpiresIn) // 토큰이 만료될시간 .signWith(key, SignatureAlgorithm.HS256) // 비밀키, 암호화 알고리즘이름 @@ -74,7 +78,7 @@ public TokenDto createToken(Authentication authentication) { String refreshToken = Jwts.builder() .setSubject(authentication.getName()) .claim(AUTHORITIES_KEY, authorities) // 권한 - .claim(USER_KEY, ((CustomUser) authentication.getPrincipal()).getUserId()) // user id + .claim(USER_KEY, ((CustomUserDetails) authentication.getPrincipal()).getUserId()) // user id .setIssuedAt(now) .setExpiration(refreshExpiration) .signWith(key, SignatureAlgorithm.HS256) @@ -112,8 +116,14 @@ public Authentication getAuthentication(String token) { .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); + // 데이터베이스에서 사용자 정보 조회 + Optional userOptional = userRepository.findById(userId); + if (userOptional.isEmpty()) { + throw new RuntimeException("User not found for id: " + userId); + } + User user = userOptional.get(); // UserDetails 객체 생성 - UserDetails principal = new CustomUser(userId, claims.getSubject(), authorities); + UserDetails principal = new CustomUserDetails(user); // UsernamePasswordAuthenticationToken을 사용하여 Authentication 객체 반환 return new UsernamePasswordAuthenticationToken(principal, "", authorities); diff --git a/src/main/java/com/chukapoka/server/common/dto/CustomUser.java b/src/main/java/com/chukapoka/server/common/dto/CustomUser.java deleted file mode 100644 index e4832d5..0000000 --- a/src/main/java/com/chukapoka/server/common/dto/CustomUser.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.chukapoka.server.common.dto; - -import lombok.Getter; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.User; - -import java.util.Collection; - -/** - * CustomUser 클래스는 Spring Security에서 제공하는 User 클래스를 확장하여 추가적인 사용자 정보를 저장하기 위한 클래스 - * 주로 사용자의 고유한 식별자(ID)를 추가로 저장하고자 할 때 사용 - */ -@Getter -public class CustomUser extends User { - private final Long userId; - - public CustomUser(Long userId, String password, Collection authorities) { - super(String.valueOf(userId), password, authorities); - this.userId = userId; - } - -} diff --git a/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java b/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java new file mode 100644 index 0000000..c85f0ff --- /dev/null +++ b/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java @@ -0,0 +1,80 @@ +package com.chukapoka.server.common.dto; + +import com.chukapoka.server.user.entity.User; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +/** + * CustomUser 클래스는 Spring Security에서 제공하는 User 클래스를 확장하여 추가적인 사용자 정보를 저장하기 위한 클래스 + * 주로 사용자의 고유한 식별자(ID)를 추가로 저장하고자 할 때 사용 + */ +@Getter +public class CustomUserDetails implements UserDetails, OAuth2User { + + private final User user; + private Map attributes; + + public CustomUserDetails(User user) { + this.user = user; + } + // OAuth 로그인 + public CustomUserDetails(User user, Map attributes) { + this.user = user; + this.attributes = attributes; + } + @Override + public Collection getAuthorities() { + return Collections.singleton(new SimpleGrantedAuthority(user.getAuthorities())); + } + + public Long getUserId() { + return user.getId(); + } + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getEmail(); + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public String getName() { + return (String) attributes.get("id"); + } + @Override + public boolean isAccountNonExpired() { + return true; + }; + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + +} diff --git a/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java b/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java index 47ba13c..fa1d5d8 100644 --- a/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java +++ b/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java @@ -1,6 +1,6 @@ package com.chukapoka.server.common.service; -import com.chukapoka.server.common.dto.CustomUser; +import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.common.enums.Authority; import com.chukapoka.server.user.entity.User; import com.chukapoka.server.user.repository.UserRepository; @@ -32,7 +32,7 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep } private UserDetails createUserDetails(User user) { - return new CustomUser(user.getId(), user.getPassword(), getAuthorities()); + return new CustomUserDetails(user); } private Collection getAuthorities() { diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index 8c5b916..ed7aebb 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -1,6 +1,6 @@ package com.chukapoka.server.tree.service; -import com.chukapoka.server.common.dto.CustomUser; +import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.tree.dto.*; import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.repository.TreeRepository; @@ -30,7 +30,7 @@ public class TreeServiceImpl implements TreeService{ @Transactional public TreeDetailResponseDto createTree(TreeCreateRequestDto treeRequestDto) { // 클라이언트에서 입력 받을 필요없이 토큰으로 접속후 권한id로 셋팅 - long userId = ((CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); Tree tree = new Tree(); treeRequestDto.setUpdatedBy(userId); diff --git a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java index 4afe406..e4fc2d7 100644 --- a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java +++ b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java @@ -1,6 +1,6 @@ package com.chukapoka.server.treeItem.service; -import com.chukapoka.server.common.dto.CustomUser; +import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.repository.TreeRepository; import com.chukapoka.server.treeItem.dto.TreeItemCreateRequestDto; @@ -33,7 +33,7 @@ public class TreeItemServiceImpl implements TreeItemService{ @Transactional public TreeItemDetailResponseDto createTreeItem(TreeItemCreateRequestDto treeItemCreateRequestDto) { String treeId = treeItemCreateRequestDto.getTreeId(); - long userId = ((CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); return saveTreeItem(treeId, userId, treeItemCreateRequestDto); } diff --git a/src/main/java/com/chukapoka/server/user/controller/HealthController.java b/src/main/java/com/chukapoka/server/user/controller/HealthController.java index bed8451..9150bf0 100644 --- a/src/main/java/com/chukapoka/server/user/controller/HealthController.java +++ b/src/main/java/com/chukapoka/server/user/controller/HealthController.java @@ -2,20 +2,11 @@ import com.chukapoka.server.common.dto.BaseResponse; -import com.chukapoka.server.common.dto.CustomUser; -import com.chukapoka.server.common.dto.TokenResponseDto; -import com.chukapoka.server.common.enums.NextActionType; + import com.chukapoka.server.common.enums.ResultType; -import com.chukapoka.server.user.dto.*; -import com.chukapoka.server.user.sevice.AuthNumberService; -import com.chukapoka.server.user.sevice.UserService; -import jakarta.mail.MessagingException; -import jakarta.validation.Valid; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.context.SecurityContextHolder; + import org.springframework.web.bind.annotation.*; -import java.io.UnsupportedEncodingException; @RestController @RequestMapping("/api") diff --git a/src/main/java/com/chukapoka/server/user/controller/UserController.java b/src/main/java/com/chukapoka/server/user/controller/UserController.java index 6cdc92b..e20c78d 100644 --- a/src/main/java/com/chukapoka/server/user/controller/UserController.java +++ b/src/main/java/com/chukapoka/server/user/controller/UserController.java @@ -59,7 +59,7 @@ public BaseResponse authNumber(@RequestParam("email") Str /** 토큰 재발급(refresh token 유효한 상태) */ @PostMapping("/reissue") public BaseResponse reissue() { - long userId = ((CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); TokenResponseDto tokenDto = userService.reissue(userId); return new BaseResponse<>(ResultType.SUCCESS, tokenDto); } @@ -68,7 +68,7 @@ public BaseResponse reissue() { @PostMapping("/logout") public BaseResponse logout() { // 인증된 사용자 Id - long userId = ((CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); // 사용자의 ID를 기반으로 로그아웃 수행 ResultType logout = userService.logout(userId); return new BaseResponse<>(ResultType.SUCCESS, logout); diff --git a/src/main/java/com/chukapoka/server/user/entity/User.java b/src/main/java/com/chukapoka/server/user/entity/User.java index b3317af..872d9bf 100644 --- a/src/main/java/com/chukapoka/server/user/entity/User.java +++ b/src/main/java/com/chukapoka/server/user/entity/User.java @@ -36,7 +36,7 @@ public class User { @Column(nullable = false) private LocalDateTime updatedAt; - @Transient + @Column private String authorities; // 권한 ROLE_USER || ROLE_ADMIN @Builder diff --git a/src/main/java/com/chukapoka/server/user/sevice/UserService.java b/src/main/java/com/chukapoka/server/user/sevice/UserService.java index 964f307..c68e170 100644 --- a/src/main/java/com/chukapoka/server/user/sevice/UserService.java +++ b/src/main/java/com/chukapoka/server/user/sevice/UserService.java @@ -3,8 +3,7 @@ import com.chukapoka.server.common.authority.JwtTokenProvider; - -import com.chukapoka.server.common.dto.CustomUser; +import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.common.dto.TokenDto; import com.chukapoka.server.common.dto.TokenResponseDto; import com.chukapoka.server.common.entity.Token; @@ -152,12 +151,7 @@ public User findUser(UserRequestDto userRequestDto) { /** 유저정보에 따른 Authentication 생성 */ public Authentication getAuthentication(User user) { return new UsernamePasswordAuthenticationToken( - new CustomUser( - user.getId(), - user.getPassword(), - List.of( - new SimpleGrantedAuthority("ROLE" + Authority.USER.getAuthority())) - ), + new CustomUserDetails(user), null, List.of( new SimpleGrantedAuthority("ROLE_" + Authority.USER.getAuthority()) From f44dc724cb028f9552a11f9738b720b51ef66739 Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Sun, 17 Mar 2024 19:27:18 +0900 Subject: [PATCH 15/25] =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EB=A7=8C=EB=A3=8C?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=BB=AC=EB=9F=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/common/authority/SecurityConfig.java | 2 ++ .../{ => jwt}/JwtAuthenticationFilter.java | 3 +-- .../authority/{ => jwt}/JwtTokenProvider.java | 11 +++++++++-- .../server/common/dto/CustomUserDetails.java | 4 +++- .../chukapoka/server/common/dto/TokenDto.java | 3 ++- .../chukapoka/server/common/entity/Token.java | 17 ++++++++++++++--- .../server/tree/service/TreeServiceImpl.java | 1 + .../server/user/sevice/UserService.java | 4 +++- 8 files changed, 35 insertions(+), 10 deletions(-) rename src/main/java/com/chukapoka/server/common/authority/{ => jwt}/JwtAuthenticationFilter.java (98%) rename src/main/java/com/chukapoka/server/common/authority/{ => jwt}/JwtTokenProvider.java (93%) diff --git a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java index ef9fd53..8a75aba 100644 --- a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java +++ b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java @@ -1,6 +1,8 @@ package com.chukapoka.server.common.authority; +import com.chukapoka.server.common.authority.jwt.JwtAuthenticationFilter; +import com.chukapoka.server.common.authority.jwt.JwtTokenProvider; import com.chukapoka.server.common.enums.Authority; import com.chukapoka.server.common.repository.TokenRepository; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/main/java/com/chukapoka/server/common/authority/JwtAuthenticationFilter.java b/src/main/java/com/chukapoka/server/common/authority/jwt/JwtAuthenticationFilter.java similarity index 98% rename from src/main/java/com/chukapoka/server/common/authority/JwtAuthenticationFilter.java rename to src/main/java/com/chukapoka/server/common/authority/jwt/JwtAuthenticationFilter.java index 2668701..4cf7fa7 100644 --- a/src/main/java/com/chukapoka/server/common/authority/JwtAuthenticationFilter.java +++ b/src/main/java/com/chukapoka/server/common/authority/jwt/JwtAuthenticationFilter.java @@ -1,4 +1,4 @@ -package com.chukapoka.server.common.authority; +package com.chukapoka.server.common.authority.jwt; import com.chukapoka.server.common.entity.Token; import com.chukapoka.server.common.repository.TokenRepository; @@ -25,7 +25,6 @@ public class JwtAuthenticationFilter extends GenericFilterBean { public static final String AUTHORIZATION_HEADER = "Authorization"; public static final String BEARER_PREFIX = "Bearer"; - private final JwtTokenProvider jwtTokenProvider; private final TokenRepository tokenRepository; diff --git a/src/main/java/com/chukapoka/server/common/authority/JwtTokenProvider.java b/src/main/java/com/chukapoka/server/common/authority/jwt/JwtTokenProvider.java similarity index 93% rename from src/main/java/com/chukapoka/server/common/authority/JwtTokenProvider.java rename to src/main/java/com/chukapoka/server/common/authority/jwt/JwtTokenProvider.java index ed68b7d..1197f49 100644 --- a/src/main/java/com/chukapoka/server/common/authority/JwtTokenProvider.java +++ b/src/main/java/com/chukapoka/server/common/authority/jwt/JwtTokenProvider.java @@ -1,4 +1,4 @@ -package com.chukapoka.server.common.authority; +package com.chukapoka.server.common.authority.jwt; import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.common.dto.TokenDto; @@ -20,6 +20,7 @@ import java.security.Key; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Optional; @@ -87,8 +88,9 @@ public TokenDto createToken(Authentication authentication) { return TokenDto.builder() .grantType(BEARER_TYPE) .accessToken(accessToken) - .accessTokenExpiresIn(accessTokenExpiresIn.getTime()) .refreshToken(refreshToken) + .atExpiration(formatDate(accessTokenExpiresIn)) + .rtExpiration(formatDate(refreshExpiration)) .build(); } @@ -166,6 +168,11 @@ public boolean isTokenExpired(String token) { Date expirationDate = claims.getExpiration(); return expirationDate != null && expirationDate.before(new Date()); } + /** 토큰 만료기한 날짜 포맷메서드 */ + private String formatDate(Date date) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); + return dateFormat.format(date); + } } \ No newline at end of file diff --git a/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java b/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java index c85f0ff..ae3b9ee 100644 --- a/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java +++ b/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java @@ -44,7 +44,7 @@ public String getPassword() { @Override public String getUsername() { - return user.getEmail(); + return user.getId().toString(); } @Override @@ -52,6 +52,8 @@ public Map getAttributes() { return attributes; } + /** tokenDB 값에서 key값을 바꾸고 싶을떄 + * Authentication 객체의 값을 UserDetails 에서 가져온다. */ @Override public String getName() { return (String) attributes.get("id"); diff --git a/src/main/java/com/chukapoka/server/common/dto/TokenDto.java b/src/main/java/com/chukapoka/server/common/dto/TokenDto.java index 32d2b5e..d3f150d 100644 --- a/src/main/java/com/chukapoka/server/common/dto/TokenDto.java +++ b/src/main/java/com/chukapoka/server/common/dto/TokenDto.java @@ -13,6 +13,7 @@ public class TokenDto { private String grantType; // JWT에 대한 인증 타입. 여기서는 Bearer를 사용. 이후 HTTP 헤더에 prefix로 붙여주는 타입 private String accessToken; private String refreshToken; - private Long accessTokenExpiresIn; + private String atExpiration; + private String rtExpiration; } diff --git a/src/main/java/com/chukapoka/server/common/entity/Token.java b/src/main/java/com/chukapoka/server/common/entity/Token.java index f819e77..d11466e 100644 --- a/src/main/java/com/chukapoka/server/common/entity/Token.java +++ b/src/main/java/com/chukapoka/server/common/entity/Token.java @@ -10,6 +10,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import javax.crypto.KEM; + @Getter @NoArgsConstructor @Data @@ -26,14 +28,24 @@ public class Token { @Column(name = "rt_value") private String rtValue; // refresh token - // TODO: 현진 access token, refresh token 만료시간 컬럼 추가 + + // 만료 시간을 나타내는 컬럼 추가 + @Column(name = "at_expiration") + private String atExpiration; // access token 만료 시간 + + @Column(name = "rt_expiration") + private String rtExpiration; // refresh token 만료 시간 + @Builder - public Token(String key, String atValue, String rtValue) { + public Token(String key, String atValue, String rtValue, String atExpiration, String rtExpiration) { this.key = key; this.atValue = atValue; this.rtValue = rtValue; + this.atExpiration = atExpiration; + this.rtExpiration = rtExpiration; } + public Token updateValues(String accessToken, String refreshToken) { this.atValue = accessToken; this.rtValue = refreshToken; @@ -44,5 +56,4 @@ public TokenResponseDto toResponseDto(){ return new TokenResponseDto(this.atValue); } - } diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index ed7aebb..2eb5c60 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -1,5 +1,6 @@ package com.chukapoka.server.tree.service; + import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.tree.dto.*; import com.chukapoka.server.tree.entity.Tree; diff --git a/src/main/java/com/chukapoka/server/user/sevice/UserService.java b/src/main/java/com/chukapoka/server/user/sevice/UserService.java index c68e170..d4515f7 100644 --- a/src/main/java/com/chukapoka/server/user/sevice/UserService.java +++ b/src/main/java/com/chukapoka/server/user/sevice/UserService.java @@ -2,7 +2,7 @@ -import com.chukapoka.server.common.authority.JwtTokenProvider; +import com.chukapoka.server.common.authority.jwt.JwtTokenProvider; import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.common.dto.TokenDto; import com.chukapoka.server.common.dto.TokenResponseDto; @@ -167,6 +167,8 @@ public TokenResponseDto saveToken(Authentication authentication){ .key(authentication.getName()) .atValue(jwtToken.getAccessToken()) .rtValue(jwtToken.getRefreshToken()) + .atExpiration(jwtToken.getAtExpiration()) + .rtExpiration(jwtToken.getRtExpiration()) .build(); return tokenRepository.save(token).toResponseDto(); From 46aa786371b020249f568542c54ca209fae3567b Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Sun, 17 Mar 2024 23:03:29 +0900 Subject: [PATCH 16/25] =?UTF-8?q?OAuth2=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=9B=84=20accessToken=20=EB=B0=9C=EA=B8=89=ED=9B=84=20header?= =?UTF-8?q?=20=EA=B0=92=20=ED=86=A0=ED=81=B0=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/authority/SecurityConfig.java | 48 ++++++++--- .../authority/jwt/JwtTokenProvider.java | 1 - .../authority/oauth2/dto/OAuth2Attribute.java | 86 +++++++++++++++++++ .../CustomAuthenticationSuccessHandler.java | 51 +++++++++++ .../service/CustomOAuth2UserService.java | 82 ++++++++++++++++++ .../server/common/dto/CustomUserDetails.java | 28 ++++-- .../service/CustomUserDetailsService.java | 3 - .../user/controller/LoginController.java | 13 +++ .../chukapoka/server/user/entity/User.java | 2 +- src/main/resources/templates/loginForm.html | 11 +++ 10 files changed, 299 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/chukapoka/server/common/authority/oauth2/dto/OAuth2Attribute.java create mode 100644 src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationSuccessHandler.java create mode 100644 src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java create mode 100644 src/main/java/com/chukapoka/server/user/controller/LoginController.java create mode 100644 src/main/resources/templates/loginForm.html diff --git a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java index 8a75aba..c048465 100644 --- a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java +++ b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java @@ -3,9 +3,11 @@ import com.chukapoka.server.common.authority.jwt.JwtAuthenticationFilter; import com.chukapoka.server.common.authority.jwt.JwtTokenProvider; +import com.chukapoka.server.common.authority.oauth2.handler.CustomAuthenticationSuccessHandler; +import com.chukapoka.server.common.authority.oauth2.service.CustomOAuth2UserService; import com.chukapoka.server.common.enums.Authority; import com.chukapoka.server.common.repository.TokenRepository; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -13,6 +15,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @@ -20,30 +23,47 @@ @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { /** * Spring Security 6.1.0부터는 메서드 체이닝의 사용을 지양하고 람다식을 통해 함수형으로 설정하게 지향함 */ - @Autowired - private JwtTokenProvider jwtTokenProvider; - @Autowired - private TokenRepository tokenRepository; + private final JwtTokenProvider jwtTokenProvider; + private final TokenRepository tokenRepository; + private final CustomOAuth2UserService customOAuth2UserService; + private final CustomAuthenticationSuccessHandler oAuth2LoginSuccessHandler; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + /** rest api 설정 */ http - .httpBasic(AbstractHttpConfigurer::disable) - .csrf(AbstractHttpConfigurer::disable) - .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, tokenRepository), UsernamePasswordAuthenticationFilter.class) - .authorizeHttpRequests((authorizeRequests) -> { - authorizeRequests - .requestMatchers("/api/user/emailCheck", "/api/user", "/api/user/authNumber", "/api/health").anonymous() - - .requestMatchers("/api/user/logout", "api/user/reissue","/api/tree","api/tree/**","api/treeItem","api/treeItem/**").hasRole(Authority.USER.getAuthority());// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 - } + .httpBasic(AbstractHttpConfigurer::disable) // 기본 인증 로그인 비활성화 + .logout(AbstractHttpConfigurer::disable) // 기본 로그아웃 비활성화 + .formLogin(AbstractHttpConfigurer::disable) // 기본 로그인 비활성화 + .csrf(AbstractHttpConfigurer::disable) // csrf 비활성화 -> cookie를 사용하지 않으면 꺼도 된다. (cookie를 사용할 경우 httpOnly(XSS 방어), sameSite(CSRF 방어)로 방어해야 한다.) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션관리 정책을 STATELESS(세션이 있으면 쓰지도 않고, 없으면 만들지도 않는다) + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, tokenRepository), UsernamePasswordAuthenticationFilter.class); + /** request 인증, 인가 설정 */ + http + .authorizeHttpRequests((authorizeRequests) -> { + authorizeRequests + .requestMatchers("/api/user/emailCheck", "/api/user", "/api/user/authNumber", "/api/health").anonymous() + .requestMatchers("/api/user/logout", "api/user/reissue", "/api/tree", "api/tree/**", "api/treeItem", "api/treeItem/**").hasRole(Authority.USER.getAuthority())// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 + .anyRequest().authenticated(); // 테스트를 위한 모든권한 설정(테스트 후 삭제 예정) + }); + /** OAuth2 로그인 설정 */ + http + .oauth2Login((oauth2) -> + oauth2 + .userInfoEndpoint(userInfoEndpointConfig -> + userInfoEndpointConfig + .userService(customOAuth2UserService)) // OAuth2 로그인시 사용자 정보를 가져오는 엔드포인트와 사용자 서비스를 설정 +// .failureHandler(oAuth2LoginFailureHandler) // OAuth2 로그인 실패시 처리할 핸들러를 지정 + .successHandler(oAuth2LoginSuccessHandler) // OAuth2 로그인 성공시 처리할 핸들러를 지정 ); return http.build(); diff --git a/src/main/java/com/chukapoka/server/common/authority/jwt/JwtTokenProvider.java b/src/main/java/com/chukapoka/server/common/authority/jwt/JwtTokenProvider.java index 1197f49..df5383f 100644 --- a/src/main/java/com/chukapoka/server/common/authority/jwt/JwtTokenProvider.java +++ b/src/main/java/com/chukapoka/server/common/authority/jwt/JwtTokenProvider.java @@ -100,7 +100,6 @@ public TokenDto createToken(Authentication authentication) { /** * JWT 토큰에서 사용자 정보를 추출하여 인증 객체를 반환하는 메서드 */ - public Authentication getAuthentication(String token) { Claims claims = parseClaims(token); diff --git a/src/main/java/com/chukapoka/server/common/authority/oauth2/dto/OAuth2Attribute.java b/src/main/java/com/chukapoka/server/common/authority/oauth2/dto/OAuth2Attribute.java new file mode 100644 index 0000000..434804f --- /dev/null +++ b/src/main/java/com/chukapoka/server/common/authority/oauth2/dto/OAuth2Attribute.java @@ -0,0 +1,86 @@ +package com.chukapoka.server.common.authority.oauth2.dto; +import com.chukapoka.server.common.enums.EmailType; +import lombok.*; + +import java.util.HashMap; +import java.util.Map; + +@ToString +@Builder(access = AccessLevel.PRIVATE) // Builder 메서드를 외부에서 사용하지 않으므로, Private 제어자로 지정 +@Getter +public class OAuth2Attribute { + private Map attributes; // 사용자 속성 정보를 담는 Map + private String attributeId; // 사용자 속성의 키 값 + private String emailType; //GOOGLE , NAVER + private String email; // 이메일 정보 + private String name; //사용자 정보 + + + + public static OAuth2Attribute of(String emailType, String userNameAttributeName, Map attributes) { + switch (emailType) { + case "google": + return ofGoogle(userNameAttributeName, attributes); + case "kakao": + return ofKakao(emailType, userNameAttributeName, attributes); + case "naver": + return ofNaver(emailType, userNameAttributeName, attributes); + default: + throw new RuntimeException(); + } + } + + /** + * Google 로그인일 경우 사용하는 메서드, 사용자 정보가 따로 Wrapping 되지 않고 제공되어, + * 바로 get() 메서드로 접근이 가능하다. + * */ + private static OAuth2Attribute ofGoogle(String userNameAttributeName, + Map attributes ) { + return OAuth2Attribute.builder() + .email((String) attributes.get("email")) + .emailType(EmailType.GOOGLE.name()) + .attributes(attributes) + .attributeId((String) attributes.get(userNameAttributeName)) + .name((String) attributes.get("name")) + .build(); + } + /** + * Kakao 로그인일 경우 사용하는 메서드, 필요한 사용자 정보가 kakaoAccount -> kakaoProfile 두번 감싸져 있어서, + * 두번 get() 메서드를 이용해 사용자 정보를 담고있는 Map을 꺼내야한다. + * */ + private static OAuth2Attribute ofKakao(String emailType, String userNameAttributeName,Map attributes) { + Map kakaoAccount = (Map) attributes.get("kakao_account"); + + return OAuth2Attribute.builder() + .email((String) kakaoAccount.get("email")) + .emailType(EmailType.KAKAO.name()) + .attributes(kakaoAccount) + .attributeId((String) kakaoAccount.get(userNameAttributeName)) + .name((String) attributes.get("name")) + .build(); + } + /* + * Naver 로그인일 경우 사용하는 메서드, 필요한 사용자 정보가 response Map에 감싸져 있어서, + * 한번 get() 메서드를 이용해 사용자 정보를 담고있는 Map을 꺼내야한다. + * */ + private static OAuth2Attribute ofNaver(String provider, String userNameAttributeName, Map attributes) { + Map response = (Map) attributes.get("response"); + + return OAuth2Attribute.builder() + .email((String) response.get("email")) + .emailType(provider) + .attributes(response) + .attributeId((String) response.get(userNameAttributeName)) + .build(); + } + + /** OAuth2User 객체에 넣어주기 위해서 Map으로 값들을 반환 */ + public Map convertToMap() { + Map map = new HashMap<>(); + map.put("id", attributeId); + map.put("emailType", emailType); + map.put("email", email); + map.put("name", name); + return map; + } +} \ No newline at end of file diff --git a/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationSuccessHandler.java b/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationSuccessHandler.java new file mode 100644 index 0000000..f2b2a9d --- /dev/null +++ b/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationSuccessHandler.java @@ -0,0 +1,51 @@ +package com.chukapoka.server.common.authority.oauth2.handler; + +import com.chukapoka.server.common.authority.jwt.JwtTokenProvider; +import com.chukapoka.server.common.dto.CustomUserDetails; +import com.chukapoka.server.common.dto.TokenDto; +import com.chukapoka.server.common.dto.TokenResponseDto; +import com.chukapoka.server.common.entity.Token; +import com.chukapoka.server.common.repository.TokenRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + + +/** OAuth2 인증이 성공했을 경우 */ +@Slf4j +@Component +@RequiredArgsConstructor +public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + private final JwtTokenProvider jwtTokenProvider; + private final TokenRepository tokenRepository; + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + + CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); + TokenResponseDto token = saveToken(authentication, userDetails.getUser().getId().toString()); + + super.onAuthenticationSuccess(request, response, authentication); + } + + /** 토큰 생성 */ + private TokenResponseDto saveToken(Authentication authentication, String id){ + System.out.println("OAuth2 토큰 생성중"); + // JWT 토큰 생성 + TokenDto jwtToken = jwtTokenProvider.createToken(authentication); + Token token = Token.builder() + .key(id) + .atValue(jwtToken.getAccessToken()) + .rtValue(jwtToken.getRefreshToken()) + .atExpiration(jwtToken.getAtExpiration()) + .rtExpiration(jwtToken.getRtExpiration()) + .build(); + return tokenRepository.save(token).toResponseDto(); +}} + diff --git a/src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java b/src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java new file mode 100644 index 0000000..3d66ce8 --- /dev/null +++ b/src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java @@ -0,0 +1,82 @@ +package com.chukapoka.server.common.authority.oauth2.service; + +import com.chukapoka.server.common.authority.oauth2.dto.OAuth2Attribute; +import com.chukapoka.server.common.dto.CustomUserDetails; +import com.chukapoka.server.common.enums.Authority; +import com.chukapoka.server.user.entity.User; +import com.chukapoka.server.user.repository.UserRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class CustomOAuth2UserService implements OAuth2UserService { + private final UserRepository userRepository; + private final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User result; + // 기본 OAuth2UserService 객체 생성 + OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService(); + + // OAuth2UserService를 사용하여 OAuth2User 정보를 가져온다. + OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); + System.out.println("oAuth2User = " + oAuth2User); + + // 클라이언트 등록 ID(google, naver, kakao)와 사용자 이름 속성을 가져온다. + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + String userNameAttributeName = userRequest.getClientRegistration() + .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); + System.out.println("registrationId = " + registrationId); + System.out.println("userNameAttributeName = " + userNameAttributeName); + + // OAuth2UserService를 사용하여 가져온 OAuth2User 정보로 OAuth2Attribute 객체를 만든다. + OAuth2Attribute oAuth2Attribute = + OAuth2Attribute.of(registrationId, userNameAttributeName, oAuth2User.getAttributes()); + + System.out.println("oAuth2Attribute = " + oAuth2Attribute); + // OAuth2Attribute의 속성값들을 Map으로 반환 받는다. + Map memberAttribute = oAuth2Attribute.convertToMap(); + System.out.println("memberAttribute = " + memberAttribute); + // 사용자 email(또는 id) 정보를 가져온다. + String email = (String) memberAttribute.get("email"); + // 이메일로 가입된 회원인지 조회한다. + Optional findMember = userRepository.findByEmail(email); + System.out.println("findMember = " + findMember); + + User user; + /** 회원이 존재하지 않을 경우 */ + if (findMember.isEmpty()) { + // user의 패스워드가 null이기 때문에 OAuth 유저는 일반적인 로그인을 할 수 없음. + user = User.builder() + .email(email) + .emailType((String) memberAttribute.get("emailType")) + .authorities("ROLE_"+Authority.USER.getAuthority()) + .build(); + userRepository.save(user); + } + /** 회원이 존재할 경우 */ + else { + user = findMember.get(); + user.setEmail(email); // Email이 변경 될 경우 업데이트 + } + return new CustomUserDetails(user, memberAttribute); + } +} + diff --git a/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java b/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java index ae3b9ee..9694a2c 100644 --- a/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java +++ b/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java @@ -12,7 +12,7 @@ import java.util.Map; /** - * CustomUser 클래스는 Spring Security에서 제공하는 User 클래스를 확장하여 추가적인 사용자 정보를 저장하기 위한 클래스 + * CustomUserDetails 클래스는 Spring Security에서 제공하는 User 클래스를 확장하여 추가적인 사용자 정보를 저장하기 위한 클래스 * 주로 사용자의 고유한 식별자(ID)를 추가로 저장하고자 할 때 사용 */ @Getter @@ -21,22 +21,29 @@ public class CustomUserDetails implements UserDetails, OAuth2User { private final User user; private Map attributes; + /**일반 로그인 */ public CustomUserDetails(User user) { this.user = user; } - // OAuth 로그인 + + /** OAuth 로그인 */ public CustomUserDetails(User user, Map attributes) { this.user = user; this.attributes = attributes; } + @Override public Collection getAuthorities() { + if (user == null) { + return Collections.emptyList(); // user가 null인 경우 빈 권한 목록 반환 + } return Collections.singleton(new SimpleGrantedAuthority(user.getAuthorities())); } public Long getUserId() { return user.getId(); } + @Override public String getPassword() { return user.getPassword(); @@ -44,7 +51,10 @@ public String getPassword() { @Override public String getUsername() { - return user.getId().toString(); + if (user != null) { + return user.getId().toString(); + } + return null; // 사용자 객체가 null인 경우 null 반환 } @Override @@ -53,26 +63,30 @@ public Map getAttributes() { } /** tokenDB 값에서 key값을 바꾸고 싶을떄 - * Authentication 객체의 값을 UserDetails 에서 가져온다. */ + * Authentication 객체의 값을 UserDetails 에서 가져온다. + */ @Override public String getName() { return (String) attributes.get("id"); } + + + /** 계정의 만료 여부 반환 (기한이 없으므로 항상 true 반환) */ @Override public boolean isAccountNonExpired() { return true; }; - + /** 계정의 잠금 여부 반환 (잠금되지 않았으므로 항상 true 반환)*/ @Override public boolean isAccountNonLocked() { return true; } - + /** 자격 증명의 만료 여부 반환 (기한이 없으므로 항상 true 반환)*/ @Override public boolean isCredentialsNonExpired() { return true; } - + /** 계정의 활성화 여부 반환 (활성화된 계정이므로 항상 true 반환)*/ @Override public boolean isEnabled() { return true; diff --git a/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java b/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java index fa1d5d8..d1688a4 100644 --- a/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java +++ b/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java @@ -35,7 +35,4 @@ private UserDetails createUserDetails(User user) { return new CustomUserDetails(user); } - private Collection getAuthorities() { - return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + Authority.USER.getAuthority())); - } } diff --git a/src/main/java/com/chukapoka/server/user/controller/LoginController.java b/src/main/java/com/chukapoka/server/user/controller/LoginController.java new file mode 100644 index 0000000..4e34c97 --- /dev/null +++ b/src/main/java/com/chukapoka/server/user/controller/LoginController.java @@ -0,0 +1,13 @@ +package com.chukapoka.server.user.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class LoginController { + + @RequestMapping("/auth/login") + public String login() { + return "loginForm"; + } +} diff --git a/src/main/java/com/chukapoka/server/user/entity/User.java b/src/main/java/com/chukapoka/server/user/entity/User.java index 872d9bf..e48babb 100644 --- a/src/main/java/com/chukapoka/server/user/entity/User.java +++ b/src/main/java/com/chukapoka/server/user/entity/User.java @@ -30,7 +30,7 @@ public class User { @Column(nullable = false) private String emailType; - @Column(nullable = false) + @Column(name = "password") private String password; @Column(nullable = false) diff --git a/src/main/resources/templates/loginForm.html b/src/main/resources/templates/loginForm.html new file mode 100644 index 0000000..736e8a5 --- /dev/null +++ b/src/main/resources/templates/loginForm.html @@ -0,0 +1,11 @@ + + + + + Login + + +

    Login with Google

    +Login with Google + + \ No newline at end of file From 66ab013088e68af12b0ec9309d6fc65b14ba6246 Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Sun, 17 Mar 2024 23:28:02 +0900 Subject: [PATCH 17/25] =?UTF-8?q?OAuth2=20=EC=9C=A0=EC=A0=80=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=EC=8B=9C=20UserResponseDto=EA=B0=9D=EC=B2=B4?= =?UTF-8?q?=EB=A5=BC=20=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EC=A0=84=EC=86=A1=20=EC=84=B1=EA=B3=B5=20```=20{?= =?UTF-8?q?=20=20=20=20=20"resultCode":=20"SUCCESS",=20=20=20=20=20"data":?= =?UTF-8?q?=20{=20=20=20=20=20=20=20=20=20"result":=20"SUCCESS",=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20"email":=20"blackduvet52@gmail.com",=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20"userId":=201,=20=20=20=20=20=20=20=20=20"tok?= =?UTF-8?q?en":=20{=20=20=20=20=20=20=20=20=20=20=20=20=20"accessToken":?= =?UTF-8?q?=20"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiYXV0aCI6IlJPTEVfVVNFUi?= =?UTF-8?q?IsInVzZXJJZCI6MSwiaWF0IjoxNzEwNjg1NDE2LCJleHAiOjE3MTA2ODcyMTZ9.?= =?UTF-8?q?qnFvAG-tffdzkcAxwSY-QnBcHNzI612-GkUqY24sS5M"=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20}=20=20=20=20=20},=20=20=20=20=20```?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomAuthenticationSuccessHandler.java | 18 ++++++++++++++++-- .../service/CustomOAuth2UserService.java | 8 -------- .../server/common/dto/CustomUserDetails.java | 6 +++++- .../service/CustomUserDetailsService.java | 7 ------- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationSuccessHandler.java b/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationSuccessHandler.java index f2b2a9d..f9f75f0 100644 --- a/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationSuccessHandler.java +++ b/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationSuccessHandler.java @@ -1,11 +1,15 @@ package com.chukapoka.server.common.authority.oauth2.handler; import com.chukapoka.server.common.authority.jwt.JwtTokenProvider; +import com.chukapoka.server.common.dto.BaseResponse; import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.common.dto.TokenDto; import com.chukapoka.server.common.dto.TokenResponseDto; import com.chukapoka.server.common.entity.Token; +import com.chukapoka.server.common.enums.ResultType; import com.chukapoka.server.common.repository.TokenRepository; +import com.chukapoka.server.user.dto.UserResponseDto; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -29,9 +33,19 @@ public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationS public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); + // 1. authentication 유저 정보에 따른 토큰 생성 TokenResponseDto token = saveToken(authentication, userDetails.getUser().getId().toString()); - - super.onAuthenticationSuccess(request, response, authentication); + // 2. baseResponse 객체 생성 + UserResponseDto userResponseDto = new UserResponseDto(ResultType.SUCCESS, userDetails.getEmail(), userDetails.getUserId(), token); + BaseResponse baseResponse = new BaseResponse<>(ResultType.SUCCESS, userResponseDto); + // 3. JSON 형식으로 변환 + ObjectMapper objectMapper = new ObjectMapper(); + String jsonResponse = objectMapper.writeValueAsString(baseResponse); + // 4. 응답 헤더 설정 + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + // 5. 클라이언트에게 응답 전송 + response.getWriter().write(jsonResponse); } /** 토큰 생성 */ diff --git a/src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java b/src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java index 3d66ce8..c88d942 100644 --- a/src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java +++ b/src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java @@ -31,25 +31,17 @@ public class CustomOAuth2UserService implements OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService(); - // OAuth2UserService를 사용하여 OAuth2User 정보를 가져온다. OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); - System.out.println("oAuth2User = " + oAuth2User); - // 클라이언트 등록 ID(google, naver, kakao)와 사용자 이름 속성을 가져온다. String registrationId = userRequest.getClientRegistration().getRegistrationId(); String userNameAttributeName = userRequest.getClientRegistration() .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); - System.out.println("registrationId = " + registrationId); - System.out.println("userNameAttributeName = " + userNameAttributeName); - // OAuth2UserService를 사용하여 가져온 OAuth2User 정보로 OAuth2Attribute 객체를 만든다. OAuth2Attribute oAuth2Attribute = OAuth2Attribute.of(registrationId, userNameAttributeName, oAuth2User.getAttributes()); - System.out.println("oAuth2Attribute = " + oAuth2Attribute); // OAuth2Attribute의 속성값들을 Map으로 반환 받는다. Map memberAttribute = oAuth2Attribute.convertToMap(); diff --git a/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java b/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java index 9694a2c..454c119 100644 --- a/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java +++ b/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java @@ -21,7 +21,7 @@ public class CustomUserDetails implements UserDetails, OAuth2User { private final User user; private Map attributes; - /**일반 로그인 */ + /** 일반 로그인 */ public CustomUserDetails(User user) { this.user = user; } @@ -40,6 +40,10 @@ public Collection getAuthorities() { return Collections.singleton(new SimpleGrantedAuthority(user.getAuthorities())); } + public String getEmail() { + return user.getEmail(); + } + public Long getUserId() { return user.getId(); } diff --git a/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java b/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java index d1688a4..c979180 100644 --- a/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java +++ b/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java @@ -1,22 +1,15 @@ package com.chukapoka.server.common.service; import com.chukapoka.server.common.dto.CustomUserDetails; -import com.chukapoka.server.common.enums.Authority; import com.chukapoka.server.user.entity.User; import com.chukapoka.server.user.repository.UserRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; -import java.util.Collection; -import java.util.Collections; - - @Service @RequiredArgsConstructor public class CustomUserDetailsService implements UserDetailsService { From 72a07fec2577808f3b43e5b177901b5aa62609e7 Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Mon, 18 Mar 2024 00:33:24 +0900 Subject: [PATCH 18/25] =?UTF-8?q?Tree=20=EC=88=98=EC=A0=95,=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8,=EC=82=AD=EC=A0=9C=20=ED=95=A0=EC=8B=9C=20us?= =?UTF-8?q?erId=EA=B0=92=EB=8F=84=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B2=8C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=ED=95=98=EC=98=80=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomAuthenticationFailHandler.java | 17 ++++++++++ .../service/CustomOAuth2UserService.java | 32 +++++++++--------- .../tree/controller/TreeController.java | 33 +++++++++++-------- .../tree/repository/TreeRepository.java | 3 +- .../server/tree/service/TreeService.java | 10 +++--- .../server/tree/service/TreeServiceImpl.java | 32 ++++++++---------- 6 files changed, 74 insertions(+), 53 deletions(-) create mode 100644 src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationFailHandler.java diff --git a/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationFailHandler.java b/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationFailHandler.java new file mode 100644 index 0000000..c6f3f2c --- /dev/null +++ b/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationFailHandler.java @@ -0,0 +1,17 @@ +package com.chukapoka.server.common.authority.oauth2.handler; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; + +import java.io.IOException; + +public class CustomAuthenticationFailHandler implements AuthenticationFailureHandler { + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { + /** 인증 실패시 메인 url로 이동 */ + response.sendRedirect("http://localhost:8080/"); + } +} diff --git a/src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java b/src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java index c88d942..7974bf0 100644 --- a/src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java +++ b/src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java @@ -8,17 +8,13 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; -import java.util.Collections; import java.util.Map; import java.util.Optional; @@ -28,29 +24,35 @@ @RequiredArgsConstructor public class CustomOAuth2UserService implements OAuth2UserService { private final UserRepository userRepository; - private final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - // 기본 OAuth2UserService 객체 생성 + log.debug("Loading user from OAuth2: {}", userRequest); + + // 1. 기본 OAuth2UserService 객체 생성 OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService(); - // OAuth2UserService를 사용하여 OAuth2User 정보를 가져온다. + + // 2. OAuth2UserService를 사용하여 OAuth2User 정보를 가져온다. OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); - // 클라이언트 등록 ID(google, naver, kakao)와 사용자 이름 속성을 가져온다. + + // 3. 클라이언트 등록 ID(google, naver, kakao)와 사용자 이름 속성을 가져온다. String registrationId = userRequest.getClientRegistration().getRegistrationId(); String userNameAttributeName = userRequest.getClientRegistration() .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); - // OAuth2UserService를 사용하여 가져온 OAuth2User 정보로 OAuth2Attribute 객체를 만든다. + + // 4. OAuth2UserService를 사용하여 가져온 OAuth2User 정보로 OAuth2Attribute 객체를 만든다. OAuth2Attribute oAuth2Attribute = OAuth2Attribute.of(registrationId, userNameAttributeName, oAuth2User.getAttributes()); - System.out.println("oAuth2Attribute = " + oAuth2Attribute); - // OAuth2Attribute의 속성값들을 Map으로 반환 받는다. + + // 5. OAuth2Attribute의 속성값들을 Map으로 반환 받는다. Map memberAttribute = oAuth2Attribute.convertToMap(); - System.out.println("memberAttribute = " + memberAttribute); - // 사용자 email(또는 id) 정보를 가져온다. + + // 6. 사용자 email(또는 id) 정보를 가져온다. String email = (String) memberAttribute.get("email"); - // 이메일로 가입된 회원인지 조회한다. + log.debug("Email retrieved from OAuth2 attributes: {}", email); + + // 7. 이메일로 가입된 회원인지 조회한다. Optional findMember = userRepository.findByEmail(email); - System.out.println("findMember = " + findMember); + User user; /** 회원이 존재하지 않을 경우 */ diff --git a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java index 40ba0f3..ab4cc89 100644 --- a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java +++ b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java @@ -1,6 +1,7 @@ package com.chukapoka.server.tree.controller; import com.chukapoka.server.common.dto.BaseResponse; +import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.common.enums.ResultType; import com.chukapoka.server.tree.dto.TreeDetailResponseDto; import com.chukapoka.server.tree.dto.TreeListResponseDto; @@ -10,6 +11,7 @@ import jakarta.validation.Valid; import lombok.AllArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; @@ -25,41 +27,44 @@ public class TreeController { /**트리 생성 */ @PostMapping public BaseResponsecreateTree(@Valid @RequestBody TreeCreateRequestDto treeRequestDto) { - TreeDetailResponseDto responseDto = treeService.createTree(treeRequestDto); + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeDetailResponseDto responseDto = treeService.createTree(treeRequestDto, userId); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } /** 트리리스트 목록 */ - @GetMapping - public BaseResponse treeList() { - TreeListResponseDto responseDto = treeService.treeList(); + @GetMapping("/list/{updatedBy}") + public BaseResponse treeList(@PathVariable("updatedBy") Long updatedBy) { + TreeListResponseDto responseDto = treeService.treeList(updatedBy); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } /** 트리상세 정보 */ - @GetMapping("/{treeId}") - private BaseResponse treeDetail(@PathVariable("treeId") String treeId) { - TreeDetailResponseDto responseDto = treeService.treeDetail(treeId); + @GetMapping("/{treeId}/{updatedBy}") + private BaseResponse treeDetail(@PathVariable("treeId") String treeId, + @PathVariable("updatedBy") Long updatedBy) { + TreeDetailResponseDto responseDto = treeService.treeDetail(treeId, updatedBy); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } /** 트리 수정 */ - @PutMapping("/{treeId}") + @PutMapping("/{treeId}/{updatedBy}") public BaseResponse treeModify(@PathVariable("treeId") String treeId, - @Valid @RequestBody TreeModifyRequestDto treeModifyDto) { - TreeDetailResponseDto responseDto = treeService.treeModify(treeId, treeModifyDto); + @PathVariable("updatedBy") Long updatedBy, + @Valid @RequestBody TreeModifyRequestDto treeModifyDto) { + TreeDetailResponseDto responseDto = treeService.treeModify(treeId,updatedBy,treeModifyDto); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } /** 트리 삭제 */ - @DeleteMapping("/{treeId}") - public BaseResponse treeDelete(@PathVariable("treeId") String treeId) { - treeService.treeDelete(treeId); + @DeleteMapping("/{treeId}/{updatedBy}") + public BaseResponse treeDelete(@PathVariable("treeId") String treeId, + @PathVariable("updatedBy") Long updatedBy) { + treeService.treeDelete(treeId,updatedBy); return new BaseResponse<>(ResultType.SUCCESS, null); } - } diff --git a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java index 534ab93..9fea608 100644 --- a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java +++ b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java @@ -14,8 +14,9 @@ @Repository public interface TreeRepository extends JpaRepository { - Optional findById(String treeId); + Optional findByTreeIdAndUpdatedBy(String treeId, Long updatedBy); + List findAllByUpdatedBy(Long updatedBy); /** treeList 조회 어떤 방법으로 할지 1. jpa @Query로 직접 찾기 2. jpa로 모두 찾은후 modelmapper로 맵핑할지 * @Query("SELECT new com.chukapoka.server.tree.dto.TreeList(tree.treeId, tree.title, tree.type, tree.linkId, tree.sendId, tree.updatedBy, tree.updatedAt) FROM Tree tree") * List findAllTrees(); diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeService.java b/src/main/java/com/chukapoka/server/tree/service/TreeService.java index fe6cbd5..630cdd7 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeService.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeService.java @@ -8,18 +8,18 @@ public interface TreeService { /** 트리 저장 */ - TreeDetailResponseDto createTree(TreeCreateRequestDto treeRequestDto); + TreeDetailResponseDto createTree(TreeCreateRequestDto treeRequestDto, long userId); /** 트리리스트 조회(리스트용 모델) */ - TreeListResponseDto treeList(); + TreeListResponseDto treeList(Long updatedBy); /** 트리 상세 정보 조회 (상세정보 모델) */ - TreeDetailResponseDto treeDetail(String treeId); + TreeDetailResponseDto treeDetail(String treeId, Long updatedBy); /** 트리 수정 */ - TreeDetailResponseDto treeModify(String treeId, TreeModifyRequestDto treeModifyDto); + TreeDetailResponseDto treeModify(String treeId,Long updatedBy, TreeModifyRequestDto treeModifyDto); /** 트리 삭제 */ - void treeDelete(String treeId); + void treeDelete(String treeId,Long updatedBy); } diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index 2eb5c60..602affa 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -1,7 +1,6 @@ package com.chukapoka.server.tree.service; -import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.tree.dto.*; import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.repository.TreeRepository; @@ -10,7 +9,6 @@ import jakarta.persistence.EntityNotFoundException; import lombok.AllArgsConstructor; import org.modelmapper.ModelMapper; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,9 +27,8 @@ public class TreeServiceImpl implements TreeService{ /** 트리생성 */ @Override @Transactional - public TreeDetailResponseDto createTree(TreeCreateRequestDto treeRequestDto) { + public TreeDetailResponseDto createTree(TreeCreateRequestDto treeRequestDto, long userId) { // 클라이언트에서 입력 받을 필요없이 토큰으로 접속후 권한id로 셋팅 - long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); Tree tree = new Tree(); treeRequestDto.setUpdatedBy(userId); @@ -43,14 +40,14 @@ public TreeDetailResponseDto createTree(TreeCreateRequestDto treeRequestDto) { /** 사용자 트리 리스트 조회(리스트용 모델) */ @Override - public TreeListResponseDto treeList() { + public TreeListResponseDto treeList(Long updatedBy) { // 1. jpa에서 TreeList를 만든다음 @query로 찾아서 가져오는 방법 // List trees = treeRepository.findAllTrees(); // return new TreeListResponseDto(trees); - //2. modelMapper로 리스트 조회후 맵핑하는방법 - List trees = treeRepository.findAll(); + List trees = treeRepository.findAllByUpdatedBy(updatedBy); + List treeLists = trees.stream() .map(tree -> modelMapper.map(tree, TreeList.class)) .collect(Collectors.toList()); @@ -59,8 +56,9 @@ public TreeListResponseDto treeList() { /** 트리 상세 정보 조회 (상세정보 모델) */ @Override - public TreeDetailResponseDto treeDetail(String treeId) { - Tree tree = findTreeByIdOrThrow(treeId); + public TreeDetailResponseDto treeDetail(String treeId, Long updatedBy) { + + Tree tree = findTreeByIdOrThrow(treeId, updatedBy); // 트리에 속한 모든 TreeItem을 가져오기 List treeItems = treeItemRepository.findByTreeId(tree.getTreeId()); TreeDetailResponseDto treeDetailResponseDto = modelMapper.map(tree, TreeDetailResponseDto.class); @@ -72,9 +70,9 @@ public TreeDetailResponseDto treeDetail(String treeId) { /** 트리수정 */ @Override @Transactional - public TreeDetailResponseDto treeModify(String treeId, TreeModifyRequestDto treeModifyDto) { + public TreeDetailResponseDto treeModify(String treeId,Long updatedBy, TreeModifyRequestDto treeModifyDto) { // 트리 아이디로 트리를 찾음 - Tree tree = findTreeByIdOrThrow(treeId); + Tree tree = findTreeByIdOrThrow(treeId, updatedBy); modelMapper.map(treeModifyDto, tree); // 변경된 트리 저장 treeRepository.save(tree); @@ -82,19 +80,17 @@ public TreeDetailResponseDto treeModify(String treeId, TreeModifyRequestDto tree return modelMapper.map(tree, TreeDetailResponseDto.class); } - - /** 트리 삭제 */ @Override @Transactional - public void treeDelete(String treeId) { - findTreeByIdOrThrow(treeId); - treeRepository.deleteById(treeId); + public void treeDelete(String treeId, Long updatedBy) { + Tree tree = findTreeByIdOrThrow(treeId, updatedBy); + treeRepository.delete(tree); } /** treeId Exception 처리 메서드 */ - private Tree findTreeByIdOrThrow(String treeId) { - return treeRepository.findById(treeId) + private Tree findTreeByIdOrThrow(String treeId, Long updatedBy) { + return treeRepository.findByTreeIdAndUpdatedBy(treeId, updatedBy) .orElseThrow(() -> new EntityNotFoundException("등록되지 않은 " + treeId + "입니다.")); } } From ef89cbcf4c889d905e034dd28d04f8d1cef7dbb1 Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Mon, 18 Mar 2024 00:34:40 +0900 Subject: [PATCH 19/25] =?UTF-8?q?OAuth2=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=EC=8B=9C=20=ED=95=B8=EB=93=A4=EB=9F=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/chukapoka/server/common/authority/SecurityConfig.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java index c048465..5a416c6 100644 --- a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java +++ b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java @@ -3,6 +3,7 @@ import com.chukapoka.server.common.authority.jwt.JwtAuthenticationFilter; import com.chukapoka.server.common.authority.jwt.JwtTokenProvider; +import com.chukapoka.server.common.authority.oauth2.handler.CustomAuthenticationFailHandler; import com.chukapoka.server.common.authority.oauth2.handler.CustomAuthenticationSuccessHandler; import com.chukapoka.server.common.authority.oauth2.service.CustomOAuth2UserService; import com.chukapoka.server.common.enums.Authority; @@ -32,6 +33,7 @@ public class SecurityConfig { private final TokenRepository tokenRepository; private final CustomOAuth2UserService customOAuth2UserService; private final CustomAuthenticationSuccessHandler oAuth2LoginSuccessHandler; + private final CustomAuthenticationFailHandler oAuthenticationFailHandler; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -62,7 +64,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig .userService(customOAuth2UserService)) // OAuth2 로그인시 사용자 정보를 가져오는 엔드포인트와 사용자 서비스를 설정 -// .failureHandler(oAuth2LoginFailureHandler) // OAuth2 로그인 실패시 처리할 핸들러를 지정 + .failureHandler(oAuthenticationFailHandler) // OAuth2 로그인 실패시 처리할 핸들러를 지정 .successHandler(oAuth2LoginSuccessHandler) // OAuth2 로그인 성공시 처리할 핸들러를 지정 ); From cb6c9daf185a98d9626f3d14ad1c714c2be47b0e Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Mon, 25 Mar 2024 23:59:36 +0900 Subject: [PATCH 20/25] =?UTF-8?q?-=20OAuth2=20=EC=82=AD=EC=A0=9C=20###=20T?= =?UTF-8?q?REE=20-=20userId=EB=A5=BC=20CustomUserDetails=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B2=8C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20service=EB=B6=80=EB=B6=84=20modelmapper=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20toentity=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OAUTH-JWT.md | 58 ------------- build.gradle | 7 +- .../common/authority/SecurityConfig.java | 27 +----- .../jwt/JwtAuthenticationFilter.java | 2 - .../authority/oauth2/dto/OAuth2Attribute.java | 86 ------------------- .../CustomAuthenticationFailHandler.java | 17 ---- .../CustomAuthenticationSuccessHandler.java | 65 -------------- .../service/CustomOAuth2UserService.java | 76 ---------------- .../server/common/dto/CustomUserDetails.java | 24 +----- .../chukapoka/server/common/entity/Token.java | 9 +- .../common/repository/TokenRepository.java | 1 - .../tree/controller/TreeController.java | 29 ++++--- .../server/tree/dto/TreeCreateRequestDto.java | 29 +++++-- .../tree/dto/TreeDetailResponseDto.java | 36 +++++++- .../chukapoka/server/tree/dto/TreeList.java | 3 - .../server/tree/dto/TreeModifyRequestDto.java | 12 ++- .../tree/repository/TreeRepository.java | 6 +- .../server/tree/service/TreeService.java | 8 +- .../server/tree/service/TreeServiceImpl.java | 48 ++++------- .../user/controller/HealthController.java | 25 ------ .../user/controller/LoginController.java | 13 --- .../chukapoka/server/user/entity/User.java | 8 +- src/main/resources/templates/loginForm.html | 11 --- 23 files changed, 117 insertions(+), 483 deletions(-) delete mode 100644 OAUTH-JWT.md delete mode 100644 src/main/java/com/chukapoka/server/common/authority/oauth2/dto/OAuth2Attribute.java delete mode 100644 src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationFailHandler.java delete mode 100644 src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationSuccessHandler.java delete mode 100644 src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java delete mode 100644 src/main/java/com/chukapoka/server/user/controller/HealthController.java delete mode 100644 src/main/java/com/chukapoka/server/user/controller/LoginController.java delete mode 100644 src/main/resources/templates/loginForm.html diff --git a/OAUTH-JWT.md b/OAUTH-JWT.md deleted file mode 100644 index 6a9ca43..0000000 --- a/OAUTH-JWT.md +++ /dev/null @@ -1,58 +0,0 @@ -## 구현 -API 서버 형태로 구현을 진행 -- 인증 :카카오/구글 소셜 로그인(코드방식) 후 jwt 발급 -- 인가 : JWT를 통한 경로별 접근 권한 -- 인증 정보 DB 저장 후 추가 정보 기입 - -### 버전 및 의존성 -- Spring boot 3.2.2 -- Spring Security 6.2.2 -- OAuth2 Client -- LomBok -- Spring Data JPA -- JJWT 0.12.3 - -### OAuth2 Code Grant 방식의 동작 순서 - -1. 로그인 페이지 -2. 성공 후 코드 발급 (redirect_url) -3. 코드를 통해 Access 토큰 요청 -4. Access 토큰 발급 완료 -5. Access 토큰을 통해 유저 정보 요청 -6. 유저 정보 획득 완료 -7. -### JWT 방식에서 OAuth2 클라이언트 구성시 고민점 - -JWT 방식에서는 로그인(인증)이 성공하면 JWT 발급 문제와 웹/하이브리드/네이티브앱별 특징에 의해 OAuth2 Code Grant 방식 동작의 책임을 프론트엔드 측에 둘 것인지 백엔드 측에 둘 것인지 많은 고민을 한다. - -- **로그인(인증)이 성공하면 JWT를 발급해야 하는 문제** - - 프론트단에서 로그인 경로에 대한 하이퍼링크를 실행하면 소셜 로그인창이 등장하고 로그인 로직이 수행된다. - - 로그인이 성공되면 JWT가 발급되는데 하이퍼링크로 실행했기 때문에 JWT를 받을 로직이 없다. (해당 부분에 대해 redirect_url 설정에 따라 많은 고민이 필요합니다.) - - API Client(axios, fetch)로 요청 보내면 백엔드측으로 요청이 전송되지만 외부 서비스 로그인 페이지를 확인할 수 없다. - -- **웹/하이브리드/네이티브앱별 특징** - - 웹에서 편하게 사용할 수 있는 웹페이지가 앱에서는 웹뷰로 보이기 때문에 UX적으로 안좋은 경험을 가질 수 있다. - - 앱 환경에서 쿠키 소멸 현상 - -## 프론트/백 책임 분배 -1. 모든 책임을 프론트가 맡음 -> 프론트단에서 (로그인 → 코드 발급 → Access 토큰 → 유저 정보 획득) 과정을 모두 수행한 뒤 백엔드단에서 (유저 정보 → JWT 발급) 방식으로 주로 네이티브앱에서 사용하는 방식. -→ 프론트에서 보낸 유저 정보의 진위 여부를 따지기 위해 추가적인 보안 로직이 필요하다. - -2. 책임을 프론트와 백엔드가 나누어 가짐 : 잘못된 방식 -> 프론트단에서 (로그인 → 코드 발급) 후 코드를 백엔드로 전송 백엔드단에서 (코드 → 토큰 발급 → 유저 정보 획득 → JWT 발급) - -3. 모든 책임을 백엔드에서 구현 -> 프론트단에서 백엔드의 OAuth2 로그인 경로로 하이퍼링킹을 진행 후 백엔드단에서 (로그인 페이지 요청 → 코드 발급 → Access 토큰 → 유저 정보 획득 → JWT 발급) 방식으로 주로 웹앱/모바일앱 통합 환경 서버에서 사용하는 방식. - -→ 백엔드에서 JWT를 발급하는 방식의 고민과 프론트측에서 받는 로직을 처리해야 한다. - - - -### 카카오 dev 톡에 적혀 있는 프론트/백 책임 분배 - -구글링을 통해 카카오 dev 톡에 적혀 있는 프론트와 백엔드가 책임을 나눠 가지는 질문에 대한 카카오 공식 답변 -![Oauth2.png](screenshots%2FOauth2.png) -앱에 대해서는 모든 책임을 프론트가 일임하고 코드나 Access 토큰을 전달하는 행위 자체를 지양 - -추가적으로 다른 자료들에도 코드나 Access 토큰을 전달하는 행위를 금지하고 있음 \ No newline at end of file diff --git a/build.gradle b/build.gradle index 64dba38..e7ccc4d 100644 --- a/build.gradle +++ b/build.gradle @@ -37,12 +37,9 @@ dependencies { // PostgreSQL JDBC 드라이버 의존성 runtimeOnly 'org.postgresql:postgresql' // h2 -// runtimeOnly 'com.h2database:h2' + // runtimeOnly 'com.h2database:h2' - // Jakarta Validation API 의존성 -// implementation("jakarta.validation:jakarta.validation-api") // 최신 버전 사용 권장 - // Spring Security 사용 시 필요한 의존성 implementation("org.springframework.boot:spring-boot-starter-security") @@ -60,8 +57,6 @@ dependencies { // modelmapper implementation 'org.modelmapper:modelmapper:2.4.4' - // OAuth2 - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' } diff --git a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java index 5a416c6..509fbac 100644 --- a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java +++ b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java @@ -3,9 +3,6 @@ import com.chukapoka.server.common.authority.jwt.JwtAuthenticationFilter; import com.chukapoka.server.common.authority.jwt.JwtTokenProvider; -import com.chukapoka.server.common.authority.oauth2.handler.CustomAuthenticationFailHandler; -import com.chukapoka.server.common.authority.oauth2.handler.CustomAuthenticationSuccessHandler; -import com.chukapoka.server.common.authority.oauth2.service.CustomOAuth2UserService; import com.chukapoka.server.common.enums.Authority; import com.chukapoka.server.common.repository.TokenRepository; import lombok.RequiredArgsConstructor; @@ -31,9 +28,6 @@ public class SecurityConfig { */ private final JwtTokenProvider jwtTokenProvider; private final TokenRepository tokenRepository; - private final CustomOAuth2UserService customOAuth2UserService; - private final CustomAuthenticationSuccessHandler oAuth2LoginSuccessHandler; - private final CustomAuthenticationFailHandler oAuthenticationFailHandler; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -45,29 +39,16 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .csrf(AbstractHttpConfigurer::disable) // csrf 비활성화 -> cookie를 사용하지 않으면 꺼도 된다. (cookie를 사용할 경우 httpOnly(XSS 방어), sameSite(CSRF 방어)로 방어해야 한다.) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션관리 정책을 STATELESS(세션이 있으면 쓰지도 않고, 없으면 만들지도 않는다) - .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, tokenRepository), UsernamePasswordAuthenticationFilter.class); + .addFilterAfter(new JwtAuthenticationFilter(jwtTokenProvider, tokenRepository), UsernamePasswordAuthenticationFilter.class); /** request 인증, 인가 설정 */ http .authorizeHttpRequests((authorizeRequests) -> { authorizeRequests - .requestMatchers("/api/user/emailCheck", "/api/user", "/api/user/authNumber", "/api/health").anonymous() - .requestMatchers("/api/user/logout", "api/user/reissue", "/api/tree", "api/tree/**", "api/treeItem", "api/treeItem/**").hasRole(Authority.USER.getAuthority())// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 - .anyRequest().authenticated(); // 테스트를 위한 모든권한 설정(테스트 후 삭제 예정) - + .requestMatchers("/api/user/emailCheck", "/api/user", "/api/user/authNumber").anonymous() + .requestMatchers("/api/user/logout", "api/user/reissue", "api/tree/**","api/treeItem/**").hasRole(Authority.USER.getAuthority()// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 + ); }); - - /** OAuth2 로그인 설정 */ - http - .oauth2Login((oauth2) -> - oauth2 - .userInfoEndpoint(userInfoEndpointConfig -> - userInfoEndpointConfig - .userService(customOAuth2UserService)) // OAuth2 로그인시 사용자 정보를 가져오는 엔드포인트와 사용자 서비스를 설정 - .failureHandler(oAuthenticationFailHandler) // OAuth2 로그인 실패시 처리할 핸들러를 지정 - .successHandler(oAuth2LoginSuccessHandler) // OAuth2 로그인 성공시 처리할 핸들러를 지정 - ); - return http.build(); } diff --git a/src/main/java/com/chukapoka/server/common/authority/jwt/JwtAuthenticationFilter.java b/src/main/java/com/chukapoka/server/common/authority/jwt/JwtAuthenticationFilter.java index 4cf7fa7..ab4c7be 100644 --- a/src/main/java/com/chukapoka/server/common/authority/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/chukapoka/server/common/authority/jwt/JwtAuthenticationFilter.java @@ -35,8 +35,6 @@ public class JwtAuthenticationFilter extends GenericFilterBean { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 1. Request Header 에서 토큰을 꺼냄 String accessToken = resolveToken((HttpServletRequest) request); -// String data = tokenRepository.getAccessToken(token); -// System.out.println("data = " + data); // 2. validateToken 으로 토큰 유효성 검사 // 정상 토큰이면 해당 토큰으로 Authentication 을 가져와서 SecurityContext 에 저장 if (StringUtils.hasText(accessToken)) { diff --git a/src/main/java/com/chukapoka/server/common/authority/oauth2/dto/OAuth2Attribute.java b/src/main/java/com/chukapoka/server/common/authority/oauth2/dto/OAuth2Attribute.java deleted file mode 100644 index 434804f..0000000 --- a/src/main/java/com/chukapoka/server/common/authority/oauth2/dto/OAuth2Attribute.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.chukapoka.server.common.authority.oauth2.dto; -import com.chukapoka.server.common.enums.EmailType; -import lombok.*; - -import java.util.HashMap; -import java.util.Map; - -@ToString -@Builder(access = AccessLevel.PRIVATE) // Builder 메서드를 외부에서 사용하지 않으므로, Private 제어자로 지정 -@Getter -public class OAuth2Attribute { - private Map attributes; // 사용자 속성 정보를 담는 Map - private String attributeId; // 사용자 속성의 키 값 - private String emailType; //GOOGLE , NAVER - private String email; // 이메일 정보 - private String name; //사용자 정보 - - - - public static OAuth2Attribute of(String emailType, String userNameAttributeName, Map attributes) { - switch (emailType) { - case "google": - return ofGoogle(userNameAttributeName, attributes); - case "kakao": - return ofKakao(emailType, userNameAttributeName, attributes); - case "naver": - return ofNaver(emailType, userNameAttributeName, attributes); - default: - throw new RuntimeException(); - } - } - - /** - * Google 로그인일 경우 사용하는 메서드, 사용자 정보가 따로 Wrapping 되지 않고 제공되어, - * 바로 get() 메서드로 접근이 가능하다. - * */ - private static OAuth2Attribute ofGoogle(String userNameAttributeName, - Map attributes ) { - return OAuth2Attribute.builder() - .email((String) attributes.get("email")) - .emailType(EmailType.GOOGLE.name()) - .attributes(attributes) - .attributeId((String) attributes.get(userNameAttributeName)) - .name((String) attributes.get("name")) - .build(); - } - /** - * Kakao 로그인일 경우 사용하는 메서드, 필요한 사용자 정보가 kakaoAccount -> kakaoProfile 두번 감싸져 있어서, - * 두번 get() 메서드를 이용해 사용자 정보를 담고있는 Map을 꺼내야한다. - * */ - private static OAuth2Attribute ofKakao(String emailType, String userNameAttributeName,Map attributes) { - Map kakaoAccount = (Map) attributes.get("kakao_account"); - - return OAuth2Attribute.builder() - .email((String) kakaoAccount.get("email")) - .emailType(EmailType.KAKAO.name()) - .attributes(kakaoAccount) - .attributeId((String) kakaoAccount.get(userNameAttributeName)) - .name((String) attributes.get("name")) - .build(); - } - /* - * Naver 로그인일 경우 사용하는 메서드, 필요한 사용자 정보가 response Map에 감싸져 있어서, - * 한번 get() 메서드를 이용해 사용자 정보를 담고있는 Map을 꺼내야한다. - * */ - private static OAuth2Attribute ofNaver(String provider, String userNameAttributeName, Map attributes) { - Map response = (Map) attributes.get("response"); - - return OAuth2Attribute.builder() - .email((String) response.get("email")) - .emailType(provider) - .attributes(response) - .attributeId((String) response.get(userNameAttributeName)) - .build(); - } - - /** OAuth2User 객체에 넣어주기 위해서 Map으로 값들을 반환 */ - public Map convertToMap() { - Map map = new HashMap<>(); - map.put("id", attributeId); - map.put("emailType", emailType); - map.put("email", email); - map.put("name", name); - return map; - } -} \ No newline at end of file diff --git a/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationFailHandler.java b/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationFailHandler.java deleted file mode 100644 index c6f3f2c..0000000 --- a/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationFailHandler.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.chukapoka.server.common.authority.oauth2.handler; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; - -import java.io.IOException; - -public class CustomAuthenticationFailHandler implements AuthenticationFailureHandler { - @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { - /** 인증 실패시 메인 url로 이동 */ - response.sendRedirect("http://localhost:8080/"); - } -} diff --git a/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationSuccessHandler.java b/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationSuccessHandler.java deleted file mode 100644 index f9f75f0..0000000 --- a/src/main/java/com/chukapoka/server/common/authority/oauth2/handler/CustomAuthenticationSuccessHandler.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.chukapoka.server.common.authority.oauth2.handler; - -import com.chukapoka.server.common.authority.jwt.JwtTokenProvider; -import com.chukapoka.server.common.dto.BaseResponse; -import com.chukapoka.server.common.dto.CustomUserDetails; -import com.chukapoka.server.common.dto.TokenDto; -import com.chukapoka.server.common.dto.TokenResponseDto; -import com.chukapoka.server.common.entity.Token; -import com.chukapoka.server.common.enums.ResultType; -import com.chukapoka.server.common.repository.TokenRepository; -import com.chukapoka.server.user.dto.UserResponseDto; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; -import org.springframework.stereotype.Component; - -import java.io.IOException; - - -/** OAuth2 인증이 성공했을 경우 */ -@Slf4j -@Component -@RequiredArgsConstructor -public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { - private final JwtTokenProvider jwtTokenProvider; - private final TokenRepository tokenRepository; - @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { - - CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); - // 1. authentication 유저 정보에 따른 토큰 생성 - TokenResponseDto token = saveToken(authentication, userDetails.getUser().getId().toString()); - // 2. baseResponse 객체 생성 - UserResponseDto userResponseDto = new UserResponseDto(ResultType.SUCCESS, userDetails.getEmail(), userDetails.getUserId(), token); - BaseResponse baseResponse = new BaseResponse<>(ResultType.SUCCESS, userResponseDto); - // 3. JSON 형식으로 변환 - ObjectMapper objectMapper = new ObjectMapper(); - String jsonResponse = objectMapper.writeValueAsString(baseResponse); - // 4. 응답 헤더 설정 - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - // 5. 클라이언트에게 응답 전송 - response.getWriter().write(jsonResponse); - } - - /** 토큰 생성 */ - private TokenResponseDto saveToken(Authentication authentication, String id){ - System.out.println("OAuth2 토큰 생성중"); - // JWT 토큰 생성 - TokenDto jwtToken = jwtTokenProvider.createToken(authentication); - Token token = Token.builder() - .key(id) - .atValue(jwtToken.getAccessToken()) - .rtValue(jwtToken.getRefreshToken()) - .atExpiration(jwtToken.getAtExpiration()) - .rtExpiration(jwtToken.getRtExpiration()) - .build(); - return tokenRepository.save(token).toResponseDto(); -}} - diff --git a/src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java b/src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java deleted file mode 100644 index 7974bf0..0000000 --- a/src/main/java/com/chukapoka/server/common/authority/oauth2/service/CustomOAuth2UserService.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.chukapoka.server.common.authority.oauth2.service; - -import com.chukapoka.server.common.authority.oauth2.dto.OAuth2Attribute; -import com.chukapoka.server.common.dto.CustomUserDetails; -import com.chukapoka.server.common.enums.Authority; -import com.chukapoka.server.user.entity.User; -import com.chukapoka.server.user.repository.UserRepository; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.stereotype.Service; - -import java.util.Map; -import java.util.Optional; - -@Slf4j -@Service -@Transactional -@RequiredArgsConstructor -public class CustomOAuth2UserService implements OAuth2UserService { - private final UserRepository userRepository; - @Override - public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - log.debug("Loading user from OAuth2: {}", userRequest); - - // 1. 기본 OAuth2UserService 객체 생성 - OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService(); - - // 2. OAuth2UserService를 사용하여 OAuth2User 정보를 가져온다. - OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); - - // 3. 클라이언트 등록 ID(google, naver, kakao)와 사용자 이름 속성을 가져온다. - String registrationId = userRequest.getClientRegistration().getRegistrationId(); - String userNameAttributeName = userRequest.getClientRegistration() - .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); - - // 4. OAuth2UserService를 사용하여 가져온 OAuth2User 정보로 OAuth2Attribute 객체를 만든다. - OAuth2Attribute oAuth2Attribute = - OAuth2Attribute.of(registrationId, userNameAttributeName, oAuth2User.getAttributes()); - - // 5. OAuth2Attribute의 속성값들을 Map으로 반환 받는다. - Map memberAttribute = oAuth2Attribute.convertToMap(); - - // 6. 사용자 email(또는 id) 정보를 가져온다. - String email = (String) memberAttribute.get("email"); - log.debug("Email retrieved from OAuth2 attributes: {}", email); - - // 7. 이메일로 가입된 회원인지 조회한다. - Optional findMember = userRepository.findByEmail(email); - - - User user; - /** 회원이 존재하지 않을 경우 */ - if (findMember.isEmpty()) { - // user의 패스워드가 null이기 때문에 OAuth 유저는 일반적인 로그인을 할 수 없음. - user = User.builder() - .email(email) - .emailType((String) memberAttribute.get("emailType")) - .authorities("ROLE_"+Authority.USER.getAuthority()) - .build(); - userRepository.save(user); - } - /** 회원이 존재할 경우 */ - else { - user = findMember.get(); - user.setEmail(email); // Email이 변경 될 경우 업데이트 - } - return new CustomUserDetails(user, memberAttribute); - } -} - diff --git a/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java b/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java index 454c119..da48094 100644 --- a/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java +++ b/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java @@ -5,33 +5,24 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.oauth2.core.user.OAuth2User; import java.util.Collection; import java.util.Collections; -import java.util.Map; /** * CustomUserDetails 클래스는 Spring Security에서 제공하는 User 클래스를 확장하여 추가적인 사용자 정보를 저장하기 위한 클래스 * 주로 사용자의 고유한 식별자(ID)를 추가로 저장하고자 할 때 사용 */ @Getter -public class CustomUserDetails implements UserDetails, OAuth2User { +public class CustomUserDetails implements UserDetails { private final User user; - private Map attributes; /** 일반 로그인 */ public CustomUserDetails(User user) { this.user = user; } - /** OAuth 로그인 */ - public CustomUserDetails(User user, Map attributes) { - this.user = user; - this.attributes = attributes; - } - @Override public Collection getAuthorities() { if (user == null) { @@ -61,19 +52,6 @@ public String getUsername() { return null; // 사용자 객체가 null인 경우 null 반환 } - @Override - public Map getAttributes() { - return attributes; - } - - /** tokenDB 값에서 key값을 바꾸고 싶을떄 - * Authentication 객체의 값을 UserDetails 에서 가져온다. - */ - @Override - public String getName() { - return (String) attributes.get("id"); - } - /** 계정의 만료 여부 반환 (기한이 없으므로 항상 true 반환) */ @Override diff --git a/src/main/java/com/chukapoka/server/common/entity/Token.java b/src/main/java/com/chukapoka/server/common/entity/Token.java index d11466e..406e3eb 100644 --- a/src/main/java/com/chukapoka/server/common/entity/Token.java +++ b/src/main/java/com/chukapoka/server/common/entity/Token.java @@ -44,14 +44,7 @@ public Token(String key, String atValue, String rtValue, String atExpiration, St this.atExpiration = atExpiration; this.rtExpiration = rtExpiration; } - - - public Token updateValues(String accessToken, String refreshToken) { - this.atValue = accessToken; - this.rtValue = refreshToken; - return this; - } - + public TokenResponseDto toResponseDto(){ return new TokenResponseDto(this.atValue); } diff --git a/src/main/java/com/chukapoka/server/common/repository/TokenRepository.java b/src/main/java/com/chukapoka/server/common/repository/TokenRepository.java index 0d58a39..52d3b3c 100644 --- a/src/main/java/com/chukapoka/server/common/repository/TokenRepository.java +++ b/src/main/java/com/chukapoka/server/common/repository/TokenRepository.java @@ -10,5 +10,4 @@ public interface TokenRepository extends JpaRepository { Optional findByKey(String key); Optional findByAtValue(String atValue); -// String getAccessToken(String token); } diff --git a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java index ab4cc89..e319445 100644 --- a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java +++ b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java @@ -33,35 +33,36 @@ public class TreeController { } /** 트리리스트 목록 */ - @GetMapping("/list/{updatedBy}") - public BaseResponse treeList(@PathVariable("updatedBy") Long updatedBy) { - TreeListResponseDto responseDto = treeService.treeList(updatedBy); + @GetMapping + public BaseResponse treeList() { + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeListResponseDto responseDto = treeService.treeList(userId); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } /** 트리상세 정보 */ - @GetMapping("/{treeId}/{updatedBy}") - private BaseResponse treeDetail(@PathVariable("treeId") String treeId, - @PathVariable("updatedBy") Long updatedBy) { - TreeDetailResponseDto responseDto = treeService.treeDetail(treeId, updatedBy); + @GetMapping("/{treeId}") + private BaseResponse treeDetail(@PathVariable("treeId") String treeId) { + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeDetailResponseDto responseDto = treeService.treeDetail(treeId, userId); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } /** 트리 수정 */ - @PutMapping("/{treeId}/{updatedBy}") + @PutMapping("/{treeId}") public BaseResponse treeModify(@PathVariable("treeId") String treeId, - @PathVariable("updatedBy") Long updatedBy, @Valid @RequestBody TreeModifyRequestDto treeModifyDto) { - TreeDetailResponseDto responseDto = treeService.treeModify(treeId,updatedBy,treeModifyDto); + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeDetailResponseDto responseDto = treeService.treeModify(treeId,userId ,treeModifyDto); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } /** 트리 삭제 */ - @DeleteMapping("/{treeId}/{updatedBy}") - public BaseResponse treeDelete(@PathVariable("treeId") String treeId, - @PathVariable("updatedBy") Long updatedBy) { - treeService.treeDelete(treeId,updatedBy); + @DeleteMapping("/{treeId}") + public BaseResponse treeDelete(@PathVariable("treeId") String treeId) { + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + treeService.treeDelete(treeId, userId); return new BaseResponse<>(ResultType.SUCCESS, null); } diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeCreateRequestDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeCreateRequestDto.java index ec2956f..ffea5be 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeCreateRequestDto.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeCreateRequestDto.java @@ -2,10 +2,12 @@ import com.chukapoka.server.common.annotation.ValidEnum; import com.chukapoka.server.common.enums.TreeType; +import com.chukapoka.server.tree.entity.Tree; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.Data; +import java.util.UUID; + @Data public class TreeCreateRequestDto { @NotBlank(message = "title is null") @@ -13,14 +15,29 @@ public class TreeCreateRequestDto { @NotBlank(message = "treeType is null") @ValidEnum(enumClass = TreeType.class, message = "TreeType must be MINE or NOT_YET_SEND") private String type; - @NotBlank(message = "linkId is null") - private String linkId; - @NotBlank(message = "sendId is null") - private String sendId; - private Long updatedBy; // 클라이언트에서는 입력받을 필요없음 ( TreeServicelmpl.createTree 에서 처리 ) private String treeBgColor; private String groundColor; private String treeTopColor; private String treeItemColor; private String treeBottomColor; + // 클라이언트에서는 입력받을 필요없음 ( TreeServicelmpl.createTree 에서 처리 ) + + /** Create Tree Build*/ + public Tree toEntity(TreeCreateRequestDto treeRequestDto, long userId) { + UUID linkId = UUID.randomUUID(); + UUID sendId = UUID.randomUUID(); + return Tree.builder() + .title(treeRequestDto.title) + .type(treeRequestDto.type) + .linkId(linkId + "-link-"+ userId ) + .sendId(sendId + "-send-"+ userId) + .treeBgColor(treeRequestDto.treeBgColor) + .groundColor(treeRequestDto.groundColor) + .treeTopColor(treeRequestDto.treeTopColor) + .treeItemColor(treeRequestDto.treeItemColor) + .treeBottomColor(treeRequestDto.treeBottomColor) + .updatedBy(userId) + .build(); + + } } \ No newline at end of file diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java index f01bb83..14248ca 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java @@ -3,7 +3,6 @@ import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.treeItem.entity.TreeItem; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @@ -11,9 +10,8 @@ import java.util.List; @Data -@AllArgsConstructor @NoArgsConstructor -@Builder +@AllArgsConstructor public class TreeDetailResponseDto { /** 트리상세 정보 */ @@ -34,4 +32,36 @@ public class TreeDetailResponseDto { private List treeItem; + /** 트리 생성, 수정 constructor */ + public TreeDetailResponseDto(Tree tree) { + this.treeId = tree.getTreeId(); + this.title = tree.getTitle(); + this.type = tree.getType(); + this.linkId = tree.getLinkId(); + this.sendId = tree.getSendId(); + this.treeBgColor = tree.getTreeBgColor(); + this.groundColor = tree.getGroundColor(); + this.treeTopColor = tree.getTreeTopColor(); + this.treeItemColor = tree.getTreeItemColor(); + this.treeBottomColor = tree.getTreeBottomColor(); + this.updatedBy = tree.getUpdatedBy(); + this.updatedAt = tree.getUpdatedAt(); + } + + /** 트리 상세정보 constructor */ + public TreeDetailResponseDto(Tree tree, List treeItem) { + this.treeId = tree.getTreeId(); + this.title = tree.getTitle(); + this.type = tree.getType(); + this.linkId = tree.getLinkId(); + this.sendId = tree.getSendId(); + this.treeBgColor = tree.getTreeBgColor(); + this.groundColor = tree.getGroundColor(); + this.treeTopColor = tree.getTreeTopColor(); + this.treeItemColor = tree.getTreeItemColor(); + this.treeBottomColor = tree.getTreeBottomColor(); + this.updatedBy = tree.getUpdatedBy(); + this.updatedAt = tree.getUpdatedAt(); + this.treeItem = treeItem; + } } diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeList.java b/src/main/java/com/chukapoka/server/tree/dto/TreeList.java index 5847a75..defffd3 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeList.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeList.java @@ -1,9 +1,6 @@ package com.chukapoka.server.tree.dto; -import lombok.AllArgsConstructor; import lombok.Data; -import lombok.NoArgsConstructor; - import java.time.LocalDateTime; @Data diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java index 252537f..52f66b6 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java @@ -2,6 +2,7 @@ import com.chukapoka.server.common.annotation.ValidEnum; import com.chukapoka.server.common.enums.TreeType; +import com.chukapoka.server.tree.entity.Tree; import jakarta.validation.constraints.NotBlank; import lombok.Data; @@ -18,5 +19,14 @@ public class TreeModifyRequestDto { private String treeTopColor; private String treeItemColor; private String treeBottomColor; - + + public void toEntity(Tree tree) { + this.title = tree.getTitle(); + this.type = tree.getType(); + this.treeBgColor = tree.getTreeBgColor(); + this.groundColor = tree.getGroundColor(); + this.treeTopColor = tree.getTreeTopColor(); + this.treeItemColor = tree.getTreeItemColor(); + this.treeBottomColor = tree.getTreeBottomColor(); + } } diff --git a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java index 9fea608..47124b0 100644 --- a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java +++ b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java @@ -1,12 +1,8 @@ package com.chukapoka.server.tree.repository; - - -import com.chukapoka.server.tree.dto.TreeList; import com.chukapoka.server.tree.entity.Tree; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.List; @@ -14,7 +10,7 @@ @Repository public interface TreeRepository extends JpaRepository { - Optional findByTreeIdAndUpdatedBy(String treeId, Long updatedBy); + Optional findByTreeIdAndUpdatedBy(String treeId, long userId); List findAllByUpdatedBy(Long updatedBy); /** treeList 조회 어떤 방법으로 할지 1. jpa @Query로 직접 찾기 2. jpa로 모두 찾은후 modelmapper로 맵핑할지 diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeService.java b/src/main/java/com/chukapoka/server/tree/service/TreeService.java index 630cdd7..d71f568 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeService.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeService.java @@ -11,15 +11,15 @@ public interface TreeService { TreeDetailResponseDto createTree(TreeCreateRequestDto treeRequestDto, long userId); /** 트리리스트 조회(리스트용 모델) */ - TreeListResponseDto treeList(Long updatedBy); + TreeListResponseDto treeList(long userId); /** 트리 상세 정보 조회 (상세정보 모델) */ - TreeDetailResponseDto treeDetail(String treeId, Long updatedBy); + TreeDetailResponseDto treeDetail(String treeId, long userId); /** 트리 수정 */ - TreeDetailResponseDto treeModify(String treeId,Long updatedBy, TreeModifyRequestDto treeModifyDto); + TreeDetailResponseDto treeModify(String treeId,long userId, TreeModifyRequestDto treeModifyDto); /** 트리 삭제 */ - void treeDelete(String treeId,Long updatedBy); + void treeDelete(String treeId,long userId); } diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index 602affa..ce8f070 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -29,25 +29,16 @@ public class TreeServiceImpl implements TreeService{ @Transactional public TreeDetailResponseDto createTree(TreeCreateRequestDto treeRequestDto, long userId) { // 클라이언트에서 입력 받을 필요없이 토큰으로 접속후 권한id로 셋팅 - Tree tree = new Tree(); - treeRequestDto.setUpdatedBy(userId); - -// BeanUtils.copyProperties(treeRequestDto, tree); // mapper대신 사용할수 있지만 복사할 속성의 수가 적고 속성 이름이 일치하는 경우에 적합 - modelMapper.map(treeRequestDto, tree); + Tree tree = treeRequestDto.toEntity(treeRequestDto, userId); treeRepository.save(tree); - return modelMapper.map(tree, TreeDetailResponseDto.class); + return new TreeDetailResponseDto(tree); } /** 사용자 트리 리스트 조회(리스트용 모델) */ @Override - public TreeListResponseDto treeList(Long updatedBy) { - // 1. jpa에서 TreeList를 만든다음 @query로 찾아서 가져오는 방법 - -// List trees = treeRepository.findAllTrees(); -// return new TreeListResponseDto(trees); - //2. modelMapper로 리스트 조회후 맵핑하는방법 - List trees = treeRepository.findAllByUpdatedBy(updatedBy); - + public TreeListResponseDto treeList(long userId) { + // modelMapper로 리스트 조회후 맵핑하는방법 + List trees = treeRepository.findAllByUpdatedBy(userId); List treeLists = trees.stream() .map(tree -> modelMapper.map(tree, TreeList.class)) .collect(Collectors.toList()); @@ -56,41 +47,40 @@ public TreeListResponseDto treeList(Long updatedBy) { /** 트리 상세 정보 조회 (상세정보 모델) */ @Override - public TreeDetailResponseDto treeDetail(String treeId, Long updatedBy) { - - Tree tree = findTreeByIdOrThrow(treeId, updatedBy); + public TreeDetailResponseDto treeDetail(String treeId, long userId) { + Tree tree = findTreeByIdOrThrow(treeId, userId); // 트리에 속한 모든 TreeItem을 가져오기 List treeItems = treeItemRepository.findByTreeId(tree.getTreeId()); - TreeDetailResponseDto treeDetailResponseDto = modelMapper.map(tree, TreeDetailResponseDto.class); - treeDetailResponseDto.setTreeItem(treeItems); - return treeDetailResponseDto; - + // 트리와 트리아이템 전체목록 반환 + return new TreeDetailResponseDto(tree, treeItems); } /** 트리수정 */ @Override @Transactional - public TreeDetailResponseDto treeModify(String treeId,Long updatedBy, TreeModifyRequestDto treeModifyDto) { + public TreeDetailResponseDto treeModify(String treeId, long userId, TreeModifyRequestDto treeModifyDto) { // 트리 아이디로 트리를 찾음 - Tree tree = findTreeByIdOrThrow(treeId, updatedBy); - modelMapper.map(treeModifyDto, tree); + Tree tree = findTreeByIdOrThrow(treeId, userId); + // TreeModifyRequestDto를 Tree 엔티티로 변환하여 엔티티에 적용 + treeModifyDto.toEntity(tree); // 변경된 트리 저장 treeRepository.save(tree); // 변경된 트리 상세 정보 반환 - return modelMapper.map(tree, TreeDetailResponseDto.class); + return new TreeDetailResponseDto(tree); } /** 트리 삭제 */ @Override @Transactional - public void treeDelete(String treeId, Long updatedBy) { - Tree tree = findTreeByIdOrThrow(treeId, updatedBy); + public void treeDelete(String treeId, long userId) { + Tree tree = findTreeByIdOrThrow(treeId, userId); treeRepository.delete(tree); } + /** treeId Exception 처리 메서드 */ - private Tree findTreeByIdOrThrow(String treeId, Long updatedBy) { - return treeRepository.findByTreeIdAndUpdatedBy(treeId, updatedBy) + private Tree findTreeByIdOrThrow(String treeId, long userId) { + return treeRepository.findByTreeIdAndUpdatedBy(treeId, userId) .orElseThrow(() -> new EntityNotFoundException("등록되지 않은 " + treeId + "입니다.")); } } diff --git a/src/main/java/com/chukapoka/server/user/controller/HealthController.java b/src/main/java/com/chukapoka/server/user/controller/HealthController.java deleted file mode 100644 index 9150bf0..0000000 --- a/src/main/java/com/chukapoka/server/user/controller/HealthController.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.chukapoka.server.user.controller; - - -import com.chukapoka.server.common.dto.BaseResponse; - -import com.chukapoka.server.common.enums.ResultType; - -import org.springframework.web.bind.annotation.*; - - -@RestController -@RequestMapping("/api") -public class HealthController { - - - /** 인증번호 요청 API */ - @GetMapping("/health") - public BaseResponse authNumber() { - return new BaseResponse<>(ResultType.SUCCESS, "health"); - } - - - -} - diff --git a/src/main/java/com/chukapoka/server/user/controller/LoginController.java b/src/main/java/com/chukapoka/server/user/controller/LoginController.java deleted file mode 100644 index 4e34c97..0000000 --- a/src/main/java/com/chukapoka/server/user/controller/LoginController.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.chukapoka.server.user.controller; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -public class LoginController { - - @RequestMapping("/auth/login") - public String login() { - return "loginForm"; - } -} diff --git a/src/main/java/com/chukapoka/server/user/entity/User.java b/src/main/java/com/chukapoka/server/user/entity/User.java index e48babb..3cfc437 100644 --- a/src/main/java/com/chukapoka/server/user/entity/User.java +++ b/src/main/java/com/chukapoka/server/user/entity/User.java @@ -24,16 +24,16 @@ public class User { @Column(name = "userId") private Long id; - @Column(nullable = false, unique = true) + @Column(nullable = false, name = "email", unique = true) private String email; - @Column(nullable = false) + @Column(nullable = false, name = "emailType") private String emailType; - @Column(name = "password") + @Column(nullable = false, name = "password") private String password; - @Column(nullable = false) + @Column(nullable = false, name = "updateAt") private LocalDateTime updatedAt; @Column diff --git a/src/main/resources/templates/loginForm.html b/src/main/resources/templates/loginForm.html deleted file mode 100644 index 736e8a5..0000000 --- a/src/main/resources/templates/loginForm.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - Login - - -

    Login with Google

    -Login with Google - - \ No newline at end of file From ada4d132f485ccda764ceac53d818bf1cfa44d89 Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Tue, 26 Mar 2024 01:25:47 +0900 Subject: [PATCH 21/25] =?UTF-8?q?=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=A4=91=20toEntity=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=EC=9D=B4=EB=82=98=20treeEntity=EC=A0=91=EA=B7=BC?= =?UTF-8?q?=ED=95=B4=EC=84=9C=20=EA=B0=92=EC=9D=84=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=A0=20=EC=8B=9C=20null=EA=B0=92=EC=9D=B4=EB=A9=B4=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=EC=A0=95=EB=B3=B4=EA=B0=80=20=EC=95=84?= =?UTF-8?q?=EB=8B=8C=20null=EA=B0=92=EC=9D=B4=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=EB=90=A8.=20if=EB=AC=B8=EC=9C=BC=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EA=B2=A0=EC=A7=80=EB=A7=8C=20?= =?UTF-8?q?=EB=84=88=EB=AC=B4=20=EA=B8=B8=EC=96=B4=EC=A7=84=EB=8B=A4....?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/tree/dto/TreeModifyRequestDto.java | 12 ++++-------- .../server/tree/service/TreeServiceImpl.java | 4 ++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java index 52f66b6..0d27154 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java @@ -4,6 +4,7 @@ import com.chukapoka.server.common.enums.TreeType; import com.chukapoka.server.tree.entity.Tree; import jakarta.validation.constraints.NotBlank; + import lombok.Data; @Data @@ -20,13 +21,8 @@ public class TreeModifyRequestDto { private String treeItemColor; private String treeBottomColor; - public void toEntity(Tree tree) { - this.title = tree.getTitle(); - this.type = tree.getType(); - this.treeBgColor = tree.getTreeBgColor(); - this.groundColor = tree.getGroundColor(); - this.treeTopColor = tree.getTreeTopColor(); - this.treeItemColor = tree.getTreeItemColor(); - this.treeBottomColor = tree.getTreeBottomColor(); + public void toEntity(Tree tree , TreeModifyRequestDto treeModifyDto) { + tree.setTitle(treeModifyDto.getTitle()); + tree.setType(treeModifyDto.getType()); } } diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index ce8f070..d67289b 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -59,16 +59,16 @@ public TreeDetailResponseDto treeDetail(String treeId, long userId) { @Override @Transactional public TreeDetailResponseDto treeModify(String treeId, long userId, TreeModifyRequestDto treeModifyDto) { - // 트리 아이디로 트리를 찾음 Tree tree = findTreeByIdOrThrow(treeId, userId); // TreeModifyRequestDto를 Tree 엔티티로 변환하여 엔티티에 적용 - treeModifyDto.toEntity(tree); + treeModifyDto.toEntity(tree, treeModifyDto); // 변경된 트리 저장 treeRepository.save(tree); // 변경된 트리 상세 정보 반환 return new TreeDetailResponseDto(tree); } + /** 트리 삭제 */ @Override @Transactional From c39b7060562133b53ea9ef019eb8b6b549bf80dd Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Tue, 26 Mar 2024 01:28:03 +0900 Subject: [PATCH 22/25] =?UTF-8?q?=EC=88=98=EC=A0=95=EA=B8=B0=EB=8A=A5=20mo?= =?UTF-8?q?delMapper=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=9D=BC?= =?UTF-8?q?=EB=8B=A8=20=EC=88=98=EC=A0=95=20..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/chukapoka/server/tree/dto/TreeModifyRequestDto.java | 5 +---- .../com/chukapoka/server/tree/service/TreeServiceImpl.java | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java index 0d27154..28d5689 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java @@ -21,8 +21,5 @@ public class TreeModifyRequestDto { private String treeItemColor; private String treeBottomColor; - public void toEntity(Tree tree , TreeModifyRequestDto treeModifyDto) { - tree.setTitle(treeModifyDto.getTitle()); - tree.setType(treeModifyDto.getType()); - } + } diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index d67289b..c83bb38 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -59,16 +59,16 @@ public TreeDetailResponseDto treeDetail(String treeId, long userId) { @Override @Transactional public TreeDetailResponseDto treeModify(String treeId, long userId, TreeModifyRequestDto treeModifyDto) { + // 트리 아이디로 트리를 찾음 Tree tree = findTreeByIdOrThrow(treeId, userId); // TreeModifyRequestDto를 Tree 엔티티로 변환하여 엔티티에 적용 - treeModifyDto.toEntity(tree, treeModifyDto); + modelMapper.map(treeModifyDto, tree); // 변경된 트리 저장 treeRepository.save(tree); // 변경된 트리 상세 정보 반환 - return new TreeDetailResponseDto(tree); + return modelMapper.map(tree, TreeDetailResponseDto.class); } - /** 트리 삭제 */ @Override @Transactional From 4383f358980aca67e258b4c3169c1700225f1c75 Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Tue, 26 Mar 2024 02:00:34 +0900 Subject: [PATCH 23/25] =?UTF-8?q?TreeItem=20CRUD=20=EC=9D=B8=EC=9E=90?= =?UTF-8?q?=EA=B0=92=EC=97=90=20userId=20=EC=B6=94=EA=B0=80=20TreeItem=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B6=80=EB=B6=84=20=EB=BA=B4=EA=B3=A0=20?= =?UTF-8?q?ModelMapper=20->=20ToEntity()=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/tree/dto/TreeModifyRequestDto.java | 1 - .../server/tree/service/TreeServiceImpl.java | 2 + .../controller/TreeItemController.java | 17 +++++--- .../dto/TreeItemCreateRequestDto.java | 17 +++++++- .../dto/TreeItemDetailResponseDto.java | 10 +++++ .../server/treeItem/entity/TreeItem.java | 2 + .../repository/TreeItemRepository.java | 2 + .../treeItem/service/TreeItemService.java | 10 ++--- .../treeItem/service/TreeItemServiceImpl.java | 42 ++++++++----------- 9 files changed, 65 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java index 28d5689..9e9cc19 100644 --- a/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java @@ -2,7 +2,6 @@ import com.chukapoka.server.common.annotation.ValidEnum; import com.chukapoka.server.common.enums.TreeType; -import com.chukapoka.server.tree.entity.Tree; import jakarta.validation.constraints.NotBlank; import lombok.Data; diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java index c83bb38..706d013 100644 --- a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -12,6 +12,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -63,6 +64,7 @@ public TreeDetailResponseDto treeModify(String treeId, long userId, TreeModifyRe Tree tree = findTreeByIdOrThrow(treeId, userId); // TreeModifyRequestDto를 Tree 엔티티로 변환하여 엔티티에 적용 modelMapper.map(treeModifyDto, tree); + tree.setUpdatedAt(LocalDateTime.now()); // 변경된 트리 저장 treeRepository.save(tree); // 변경된 트리 상세 정보 반환 diff --git a/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java b/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java index cfbed9c..353e0d9 100644 --- a/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java +++ b/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java @@ -1,6 +1,7 @@ package com.chukapoka.server.treeItem.controller; import com.chukapoka.server.common.dto.BaseResponse; +import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.common.enums.ResultType; import com.chukapoka.server.treeItem.dto.TreeItemCreateRequestDto; import com.chukapoka.server.treeItem.dto.TreeItemDetailResponseDto; @@ -9,6 +10,7 @@ import com.chukapoka.server.treeItem.service.TreeItemService; import jakarta.validation.Valid; import lombok.AllArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; @RestController @@ -20,21 +22,24 @@ public class TreeItemController { /** 트리아이템 생성 */ @PostMapping public BaseResponse createTreeItem(@Valid @RequestBody TreeItemCreateRequestDto treeItemCreateRequestDto) { - TreeItemDetailResponseDto responseDto = treeItemService.createTreeItem(treeItemCreateRequestDto); + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeItemDetailResponseDto responseDto = treeItemService.createTreeItem(treeItemCreateRequestDto, userId); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } /** 트리리스트 목록 */ @GetMapping public BaseResponse treeItemList() { - TreeItemListResponseDto responseDto = treeItemService.treeList(); + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeItemListResponseDto responseDto = treeItemService.treeList(userId); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } /** 트리상세 정보 */ @GetMapping("/{treeItemId}") private BaseResponse treeItemDetail(@PathVariable("treeItemId") String treeItemId) { - TreeItemDetailResponseDto responseDto = treeItemService.treeDetail(treeItemId); + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeItemDetailResponseDto responseDto = treeItemService.treeDetail(treeItemId, userId); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } @@ -42,7 +47,8 @@ private BaseResponse treeItemDetail(@PathVariable("tr @PutMapping("/{treeItemId}") public BaseResponse treeItemModify(@PathVariable("treeItemId") String treeItemId, @Valid @RequestBody TreeItemModifyRequestDto treeItemModifyDto) { - TreeItemDetailResponseDto responseDto = treeItemService.treeModify(treeItemId, treeItemModifyDto); + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeItemDetailResponseDto responseDto = treeItemService.treeModify(treeItemId, treeItemModifyDto, userId); return new BaseResponse<>(ResultType.SUCCESS, responseDto); } @@ -50,7 +56,8 @@ public BaseResponse treeItemModify(@PathVariable("tre /** 트리 삭제 */ @DeleteMapping("/{treeItemId}") public BaseResponse treeItemDelete(@PathVariable("treeItemId") String treeItemId) { - treeItemService.treeItemDelete(treeItemId); + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + treeItemService.treeItemDelete(treeItemId, userId); return new BaseResponse<>(ResultType.SUCCESS, null); } } diff --git a/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java index 1e27515..95b0268 100644 --- a/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java +++ b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java @@ -1,5 +1,7 @@ package com.chukapoka.server.treeItem.dto; +import com.chukapoka.server.tree.entity.Tree; +import com.chukapoka.server.treeItem.entity.TreeItem; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -17,8 +19,19 @@ public class TreeItemCreateRequestDto { private String content; @NotBlank(message = "treeItemColor is null") private String treeItemColor; - private Long updatedBy; - private LocalDateTime updatedAt; + + + public TreeItem toEntity(Tree tree, TreeItemCreateRequestDto treeItemCreateRequestDto, long userId) { + return TreeItem.builder() + .treeId(tree.getTreeId()) + .title(treeItemCreateRequestDto.getTitle()) + .content(treeItemCreateRequestDto.getContent()) + .treeItemColor(treeItemCreateRequestDto.getTreeItemColor()) + .updatedBy(userId) + .updatedAt(LocalDateTime.now()) + .build(); + } + } diff --git a/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemDetailResponseDto.java b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemDetailResponseDto.java index 3d36551..1c8028d 100644 --- a/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemDetailResponseDto.java +++ b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemDetailResponseDto.java @@ -1,5 +1,6 @@ package com.chukapoka.server.treeItem.dto; +import com.chukapoka.server.treeItem.entity.TreeItem; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -20,4 +21,13 @@ public class TreeItemDetailResponseDto { private Long updatedBy; private LocalDateTime updatedAt; + public TreeItemDetailResponseDto(TreeItem treeItem) { + this.id = treeItem.getId(); + this.treeId = treeItem.getTreeId(); + this.title = treeItem.getTitle(); + this.content = treeItem.getContent(); + this.treeItemColor = treeItem.getTreeItemColor(); + this.updatedBy = treeItem.getUpdatedBy(); + this.updatedAt = treeItem.getUpdatedAt(); + } } diff --git a/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java b/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java index bac7a88..50784e0 100644 --- a/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java +++ b/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.DynamicUpdate; @@ -15,6 +16,7 @@ @Data @AllArgsConstructor @NoArgsConstructor +@Builder @DynamicUpdate // 데이터의 변경사항이 있는 것만 수정 @Table(name = "tb_treeItem") public class TreeItem { diff --git a/src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java b/src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java index 652b776..364846f 100644 --- a/src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java +++ b/src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java @@ -4,7 +4,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface TreeItemRepository extends JpaRepository { List findByTreeId(String treeId); + Optional findByIdAndUpdatedBy(String treeItemId, long userId); } diff --git a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java index ac11b4e..5811bbe 100644 --- a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java +++ b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java @@ -7,17 +7,17 @@ public interface TreeItemService { /** 트리 아이템 생성 */ - TreeItemDetailResponseDto createTreeItem(TreeItemCreateRequestDto treeItemCreateRequestDto); + TreeItemDetailResponseDto createTreeItem(TreeItemCreateRequestDto treeItemCreateRequestDto, long userId); /** 트리아이템리스트 조회(리스트용 모델) */ - TreeItemListResponseDto treeList(); + TreeItemListResponseDto treeList(long userId); /** 트리아이템 상세 정보 조회 (상세정보 모델) */ - TreeItemDetailResponseDto treeDetail(String treeItemId); + TreeItemDetailResponseDto treeDetail(String treeItemId, long userId); /** 트리아이템 수정 */ - TreeItemDetailResponseDto treeModify(String treeItemId, TreeItemModifyRequestDto treeItemModifyDto); + TreeItemDetailResponseDto treeModify(String treeItemId, TreeItemModifyRequestDto treeItemModifyDto, long userId); /** 트리아이템 삭제 */ - void treeItemDelete(String treeItemId); + void treeItemDelete(String treeItemId, long userId); } diff --git a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java index e4fc2d7..9dd36ce 100644 --- a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java +++ b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java @@ -1,6 +1,5 @@ package com.chukapoka.server.treeItem.service; -import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.tree.entity.Tree; import com.chukapoka.server.tree.repository.TreeRepository; import com.chukapoka.server.treeItem.dto.TreeItemCreateRequestDto; @@ -12,7 +11,7 @@ import jakarta.persistence.EntityNotFoundException; import lombok.AllArgsConstructor; import org.modelmapper.ModelMapper; -import org.springframework.security.core.context.SecurityContextHolder; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,15 +30,15 @@ public class TreeItemServiceImpl implements TreeItemService{ /** 트리 아이템 생성 */ @Override @Transactional - public TreeItemDetailResponseDto createTreeItem(TreeItemCreateRequestDto treeItemCreateRequestDto) { + public TreeItemDetailResponseDto createTreeItem(TreeItemCreateRequestDto treeItemCreateRequestDto, long userId) { String treeId = treeItemCreateRequestDto.getTreeId(); - long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); return saveTreeItem(treeId, userId, treeItemCreateRequestDto); } + /** 트리아이템 (리스트) */ @Override - public TreeItemListResponseDto treeList() { + public TreeItemListResponseDto treeList(long userId) { List treeItems = treeItemRepository.findAll(); List treeItemDetailResponseDtos = treeItems.stream() .map(treeItem -> modelMapper.map(treeItem, TreeItemDetailResponseDto.class)) @@ -50,16 +49,16 @@ public TreeItemListResponseDto treeList() { /** 트라이이템 (상세정보) */ @Override - public TreeItemDetailResponseDto treeDetail(String treeItemId) { - TreeItem treeItem = findTreeItemIdOrThrow(treeItemId); - return modelMapper.map(treeItem, TreeItemDetailResponseDto.class); + public TreeItemDetailResponseDto treeDetail(String treeItemId, long userId) { + TreeItem treeItem = findTreeItemIdOrThrow(treeItemId, userId); + return new TreeItemDetailResponseDto(treeItem); } /** 트리아이템 수정 */ @Override @Transactional - public TreeItemDetailResponseDto treeModify(String treeItemId, TreeItemModifyRequestDto treeItemModifyDto) { - TreeItem treeItem = findTreeItemIdOrThrow(treeItemId); + public TreeItemDetailResponseDto treeModify(String treeItemId, TreeItemModifyRequestDto treeItemModifyDto, long userId) { + TreeItem treeItem = findTreeItemIdOrThrow(treeItemId, userId); modelMapper.map(treeItemModifyDto, treeItem); treeItem.setUpdatedAt(LocalDateTime.now()); // 변경된 트리아이템 저장 @@ -71,31 +70,24 @@ public TreeItemDetailResponseDto treeModify(String treeItemId, TreeItemModifyReq /** 트리아이템 삭제 */ @Override @Transactional - public void treeItemDelete(String treeItemId) { - findTreeItemIdOrThrow(treeItemId); - treeItemRepository.deleteById(treeItemId); + public void treeItemDelete(String treeItemId, long userId) { + TreeItem treeItem = findTreeItemIdOrThrow(treeItemId, userId); + treeItemRepository.delete(treeItem); } - /** 트라이이템 저장 메서드 */ private TreeItemDetailResponseDto saveTreeItem(String treeId, long userId, TreeItemCreateRequestDto treeItemCreateRequestDto) { // 트리 객체 조회 - Tree tree = treeRepository.findById(treeId) - .orElseThrow(() -> new EntityNotFoundException("Tree not found with id: " + treeId)); + Tree tree = treeRepository.findById(treeId).orElseThrow(() -> new EntityNotFoundException("등록되지 않은 " + treeId + "입니다.")); // 트리 아이템 생성 및 저장 - TreeItem treeItem = modelMapper.map(treeItemCreateRequestDto, TreeItem.class); - treeItem.setTreeId(treeId); - treeItem.setUpdatedBy(userId); - treeItem.setUpdatedAt(LocalDateTime.now()); + TreeItem treeItem = treeItemCreateRequestDto.toEntity(tree , treeItemCreateRequestDto, userId); treeItemRepository.save(treeItem); - - return modelMapper.map(treeItem, TreeItemDetailResponseDto.class); + return new TreeItemDetailResponseDto(treeItem); } - /** treeItemId Exception 처리 메서드 */ - private TreeItem findTreeItemIdOrThrow(String treeItemId) { - return treeItemRepository.findById(treeItemId) + private TreeItem findTreeItemIdOrThrow(String treeItemId, long userId) { + return treeItemRepository.findByIdAndUpdatedBy(treeItemId, userId) .orElseThrow(() -> new EntityNotFoundException("등록되지 않은 " + treeItemId + "입니다.")); } } From 326509ecf1cf01c37e39da89362069dc02f6fd96 Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Wed, 27 Mar 2024 19:15:22 +0900 Subject: [PATCH 24/25] =?UTF-8?q?Swagger3.0=20version=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../common/authority/SecurityConfig.java | 3 +- .../common/authority/SwaggerConfig.java | 32 +++++++ swagger.yaml | 90 ------------------- 4 files changed, 37 insertions(+), 91 deletions(-) create mode 100644 src/main/java/com/chukapoka/server/common/authority/SwaggerConfig.java delete mode 100644 swagger.yaml diff --git a/build.gradle b/build.gradle index e7ccc4d..c1d8249 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,9 @@ dependencies { // modelmapper implementation 'org.modelmapper:modelmapper:2.4.4' + + // swagger3 + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' } diff --git a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java index 509fbac..d375666 100644 --- a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java +++ b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java @@ -45,8 +45,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorizeRequests) -> { authorizeRequests - .requestMatchers("/api/user/emailCheck", "/api/user", "/api/user/authNumber").anonymous() + .requestMatchers("/api/user/emailCheck", "/api/user", "/api/user/authNumber","/swagger-ui/**", "/v3/api-docs/**").anonymous() .requestMatchers("/api/user/logout", "api/user/reissue", "api/tree/**","api/treeItem/**").hasRole(Authority.USER.getAuthority()// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 + ); }); return http.build(); diff --git a/src/main/java/com/chukapoka/server/common/authority/SwaggerConfig.java b/src/main/java/com/chukapoka/server/common/authority/SwaggerConfig.java new file mode 100644 index 0000000..d96a6ff --- /dev/null +++ b/src/main/java/com/chukapoka/server/common/authority/SwaggerConfig.java @@ -0,0 +1,32 @@ +package com.chukapoka.server.common.authority; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** swagger 의존성만 설정해도 자동 적용되지만, jwt 토큰값을 확인하기 위한 설정 */ +@Configuration +public class SwaggerConfig { + + + /** SwaggerConfig의 openAPI 함수에 security schemes를 추가 + * addList 부분과 addSecuritySchemes의 이름 부분은 변경이 가능하지만 둘 다 같은 이름이어야 함 */ + @Bean + public OpenAPI openAPI(){ + return new OpenAPI().addSecurityItem(new SecurityRequirement().addList("JWT")) + .components(new Components().addSecuritySchemes("JWT", createAPIKeyScheme())) + .info(new Info().title("Chukapoka API") + .description("This is Chukapoka API") + .version("v2.2.0")); + } + /** JWT를 적용하려면 confugure에 JWT SecurityScheme이 필요 */ + private SecurityScheme createAPIKeyScheme() { + return new SecurityScheme().type(SecurityScheme.Type.HTTP) + .bearerFormat("JWT") + .scheme("bearer"); + } +} diff --git a/swagger.yaml b/swagger.yaml deleted file mode 100644 index c1c77f6..0000000 --- a/swagger.yaml +++ /dev/null @@ -1,90 +0,0 @@ -openapi: 3.0.3 -info: - title: Vue-Springboot-Memo Api - description: |- - Vue Springboot client와 server 테스트 - https://github.com/doyou1/vue-spring-sampling - version: 1.0.0 -servers: - - url: http://jh-memo-env.eba-khreh2xt.ap-northeast-1.elasticbeanstalk.com/api -tags: - - name: /api/memo - description: about memo -paths: - /api/memo: - get: - tags: - - /api/memo - summary: get memo list - description: get memo list - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Memo' - post: - tags: - - /api/memo - summary: add a Memo item - description: add a Memo item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - /api/memo/{id}: - put: - tags: - - /api/memo - summary: update a Memo item - description: update a Memo item - parameters: - - name: id - in: path - required: true - description: id to update the Memo item - schema: - type : integer - format: int64 - example: 10 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Memo' - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' -components: - schemas: - Memo: - properties: - id: - type: integer - format: int64 - example: 10 - content: - type: string - example: memo content - isDone: - type: boolean - ApiResponse: - type: object - properties: - id: - type: integer - format: int32 - example: 10 - isSuccess: - type: boolean \ No newline at end of file From 663e3bb12ee4dfcdd733657f1846ecd8e063de08 Mon Sep 17 00:00:00 2001 From: Hyun jin Date: Wed, 27 Mar 2024 22:37:42 +0900 Subject: [PATCH 25/25] =?UTF-8?q?h2=20DataBase=20=EB=B3=80=EA=B2=BD=20->?= =?UTF-8?q?=20h2=20=EC=9A=94=EC=86=8C=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++-- .../chukapoka/server/common/authority/SecurityConfig.java | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index c1d8249..8617aea 100644 --- a/build.gradle +++ b/build.gradle @@ -35,9 +35,9 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-validation") // PostgreSQL JDBC 드라이버 의존성 - runtimeOnly 'org.postgresql:postgresql' +// runtimeOnly 'org.postgresql:postgresql' // h2 - // runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.h2database:h2' // Spring Security 사용 시 필요한 의존성 diff --git a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java index d375666..713b09a 100644 --- a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java +++ b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java @@ -13,6 +13,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @@ -33,6 +34,7 @@ public class SecurityConfig { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { /** rest api 설정 */ http + .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) // h2 요소를 사용 비활성화 .httpBasic(AbstractHttpConfigurer::disable) // 기본 인증 로그인 비활성화 .logout(AbstractHttpConfigurer::disable) // 기본 로그아웃 비활성화 .formLogin(AbstractHttpConfigurer::disable) // 기본 로그인 비활성화 @@ -45,7 +47,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorizeRequests) -> { authorizeRequests - .requestMatchers("/api/user/emailCheck", "/api/user", "/api/user/authNumber","/swagger-ui/**", "/v3/api-docs/**").anonymous() + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**","/h2-console/**").permitAll() + .requestMatchers("/api/user/emailCheck", "/api/user", "/api/user/authNumber").anonymous() .requestMatchers("/api/user/logout", "api/user/reissue", "api/tree/**","api/treeItem/**").hasRole(Authority.USER.getAuthority()// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 );

    ?hWtIsMRp#lJLv9V$C=MF-c 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