diff --git a/src/main/java/com/_119/wepro/global/config/WebSecurityConfig.java b/src/main/java/com/_119/wepro/global/config/WebSecurityConfig.java index 03e9d63..21ab57c 100644 --- a/src/main/java/com/_119/wepro/global/config/WebSecurityConfig.java +++ b/src/main/java/com/_119/wepro/global/config/WebSecurityConfig.java @@ -7,6 +7,7 @@ import com._119.wepro.global.handler.CustomLogoutHandler; import com._119.wepro.global.handler.CustomLogoutSuccessHandler; import com._119.wepro.global.security.CustomOidcAuthenticationSuccessHandler; +import com._119.wepro.global.security.CustomOidcUserService; import com._119.wepro.global.security.JwtTokenProvider; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -25,6 +26,7 @@ public class WebSecurityConfig { private final JwtTokenProvider jwtTokenProvider; + private final CustomOidcUserService customOidcUserService; private final CustomAuthenticationFailureHandler customAuthenticationFailureHandler; @Bean @@ -55,6 +57,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .httpBasic(withDefaults()) .oauth2Login(oauth2Login -> oauth2Login + .userInfoEndpoint(userInfo -> userInfo.oidcUserService(customOidcUserService)) .failureHandler(customAuthenticationFailureHandler) .successHandler(customOidcAuthenticationSuccessHandler()) ) diff --git a/src/main/java/com/_119/wepro/global/exception/errorcode/ProjectErrorCode.java b/src/main/java/com/_119/wepro/global/exception/errorcode/ProjectErrorCode.java index 8b40111..0ea756b 100644 --- a/src/main/java/com/_119/wepro/global/exception/errorcode/ProjectErrorCode.java +++ b/src/main/java/com/_119/wepro/global/exception/errorcode/ProjectErrorCode.java @@ -9,6 +9,7 @@ public enum ProjectErrorCode implements ErrorCode { PROJECT_NOT_FOUND(HttpStatus.NOT_FOUND, "Project not found"), + PROJECT_MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "등록하시려고 하는 멤버가 존재하지 않습니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/_119/wepro/global/security/CustomOidcAuthenticationSuccessHandler.java b/src/main/java/com/_119/wepro/global/security/CustomOidcAuthenticationSuccessHandler.java index 19b7cca..5d1820b 100644 --- a/src/main/java/com/_119/wepro/global/security/CustomOidcAuthenticationSuccessHandler.java +++ b/src/main/java/com/_119/wepro/global/security/CustomOidcAuthenticationSuccessHandler.java @@ -9,7 +9,9 @@ import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; @@ -22,18 +24,24 @@ public class CustomOidcAuthenticationSuccessHandler extends SimpleUrlAuthenticat @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, - Authentication authentication) throws IOException { + Authentication authentication) throws IOException, ServletException { log.info("authentication success"); - CustomOidcUser user = (CustomOidcUser) authentication.getPrincipal(); + CustomOidcUser customOidcUser = (CustomOidcUser) authentication.getPrincipal(); - // 토큰 발급 - TokenInfo tokenInfo = jwtTokenProvider.generateToken(user.getAttribute("sub"), - user.getMemberRole()); + // JWT 토큰 발급 + TokenInfo tokenInfo = jwtTokenProvider.generateToken(String.valueOf(customOidcUser.getMemberId()), + customOidcUser.getMemberRole()); + // SecurityContext에 명시적으로 CustomOidcUser 저장 (이미 존재하지만, 명확히 보장하기 위함) + Authentication newAuth = new UsernamePasswordAuthenticationToken(customOidcUser, + authentication.getCredentials(), authentication.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(newAuth); + + // 리다이렉트 response.sendRedirect( "http://localhost:8081/oauth2/authorization/login?token=" + tokenInfo.getAccessToken() - + "&refresh=" + tokenInfo.getRefreshToken() + "&isGuest=" + (user.isGuest() ? "true" - : "false")); + + "&refresh=" + tokenInfo.getRefreshToken() + "&isGuest=" + (customOidcUser.isGuest() + ? "true" : "false")); } } diff --git a/src/main/java/com/_119/wepro/global/security/CustomOidcUserService.java b/src/main/java/com/_119/wepro/global/security/CustomOidcUserService.java index 3648297..500d9cb 100644 --- a/src/main/java/com/_119/wepro/global/security/CustomOidcUserService.java +++ b/src/main/java/com/_119/wepro/global/security/CustomOidcUserService.java @@ -6,6 +6,7 @@ import com._119.wepro.member.domain.Provider; import com._119.wepro.member.domain.repository.MemberRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.core.oidc.user.OidcUser; @@ -13,6 +14,7 @@ @Service @RequiredArgsConstructor +@Slf4j public class CustomOidcUserService extends OidcUserService { private final MemberRepository memberRepository; @@ -21,6 +23,7 @@ public class CustomOidcUserService extends OidcUserService { public OidcUser loadUser(OidcUserRequest userRequest) { OidcUser oidcUser = super.loadUser(userRequest); + Member newMember = fetchOrCreate(oidcUser); return new CustomOidcUser(oidcUser, newMember.getId(), newMember.getRole()); diff --git a/src/main/java/com/_119/wepro/image/domain/Image.java b/src/main/java/com/_119/wepro/image/domain/Image.java index 2e75064..95ccc4b 100644 --- a/src/main/java/com/_119/wepro/image/domain/Image.java +++ b/src/main/java/com/_119/wepro/image/domain/Image.java @@ -2,7 +2,18 @@ import static lombok.AccessLevel.PROTECTED; +import com._119.wepro.image.dto.request.ImageCreateRequest; +import com._119.wepro.project.domain.Project; +import com._119.wepro.project.dto.request.ProjectRequest.ProjectCreateRequest; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -11,7 +22,23 @@ @Setter @AllArgsConstructor @NoArgsConstructor(access = PROTECTED) +@Entity +@Builder public class Image { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; private String url; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "project_id") + private Project project; + + public static Image of(ImageCreateRequest imageCreateRequest) { + return Image.builder() + .url(imageCreateRequest.getUrl()) + .project(imageCreateRequest.getProject()) + .build(); + } } diff --git a/src/main/java/com/_119/wepro/image/dto/request/ImageCreateRequest.java b/src/main/java/com/_119/wepro/image/dto/request/ImageCreateRequest.java new file mode 100644 index 0000000..bbe42c0 --- /dev/null +++ b/src/main/java/com/_119/wepro/image/dto/request/ImageCreateRequest.java @@ -0,0 +1,10 @@ +package com._119.wepro.image.dto.request; + +import com._119.wepro.project.domain.Project; +import lombok.Data; + +@Data +public class ImageCreateRequest { + String url; + Project project; +} diff --git a/src/main/java/com/_119/wepro/image/dto/ImagesResponse.java b/src/main/java/com/_119/wepro/image/dto/response/ImagesResponse.java similarity index 91% rename from src/main/java/com/_119/wepro/image/dto/ImagesResponse.java rename to src/main/java/com/_119/wepro/image/dto/response/ImagesResponse.java index 934647a..bf5556b 100644 --- a/src/main/java/com/_119/wepro/image/dto/ImagesResponse.java +++ b/src/main/java/com/_119/wepro/image/dto/response/ImagesResponse.java @@ -1,4 +1,4 @@ -package com._119.wepro.image.dto; +package com._119.wepro.image.dto.response; import com._119.wepro.image.domain.Image; import java.util.List; diff --git a/src/main/java/com/_119/wepro/image/presentation/ImageController.java b/src/main/java/com/_119/wepro/image/presentation/ImageController.java index b6cbcdc..0bf579f 100644 --- a/src/main/java/com/_119/wepro/image/presentation/ImageController.java +++ b/src/main/java/com/_119/wepro/image/presentation/ImageController.java @@ -1,6 +1,6 @@ package com._119.wepro.image.presentation; -import com._119.wepro.image.dto.ImagesResponse; +import com._119.wepro.image.dto.response.ImagesResponse; import com._119.wepro.image.service.ImageService; import java.net.URI; import java.util.List; diff --git a/src/main/java/com/_119/wepro/image/service/ImageService.java b/src/main/java/com/_119/wepro/image/service/ImageService.java index aacb036..330a774 100644 --- a/src/main/java/com/_119/wepro/image/service/ImageService.java +++ b/src/main/java/com/_119/wepro/image/service/ImageService.java @@ -7,7 +7,7 @@ import com._119.wepro.global.exception.RestApiException; import com._119.wepro.image.domain.ImageFile; import com._119.wepro.image.domain.S3ImageEvent; -import com._119.wepro.image.dto.ImagesResponse; +import com._119.wepro.image.dto.response.ImagesResponse; import com._119.wepro.image.infrastructure.ImageUploader; import java.util.List; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/_119/wepro/member/domain/Member.java b/src/main/java/com/_119/wepro/member/domain/Member.java index 06af8ed..e0565fa 100644 --- a/src/main/java/com/_119/wepro/member/domain/Member.java +++ b/src/main/java/com/_119/wepro/member/domain/Member.java @@ -1,6 +1,7 @@ package com._119.wepro.member.domain; import com._119.wepro.global.BaseEntity; +import com._119.wepro.project.domain.ProjectMember; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; @@ -10,14 +11,17 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; import jakarta.persistence.PostPersist; import jakarta.persistence.Table; import java.time.LocalDateTime; +import java.util.Set; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; @Entity @Getter @@ -29,6 +33,7 @@ @Index(name = "idx_provider_id", columnList = "providerId") } ) +@Setter public class Member extends BaseEntity { @Id @@ -55,6 +60,9 @@ public class Member extends BaseEntity { private LocalDateTime inactivatedAt; + @OneToMany(mappedBy = "member") + private Set projectMembers; + // 엔티티가 저장된 후 id로 태그를 생성합니다. //todo 태그 저장안되는 이슈 확인하기 @PostPersist diff --git a/src/main/java/com/_119/wepro/member/domain/repository/MemberCustomRepository.java b/src/main/java/com/_119/wepro/member/domain/repository/MemberCustomRepository.java new file mode 100644 index 0000000..9c29a6c --- /dev/null +++ b/src/main/java/com/_119/wepro/member/domain/repository/MemberCustomRepository.java @@ -0,0 +1,21 @@ +package com._119.wepro.member.domain.repository; + +import static com._119.wepro.member.domain.QMember.member; + +import com._119.wepro.member.domain.Member; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class MemberCustomRepository { + + private final JPAQueryFactory queryFactory; + + public List findMembers(String keyword) { + return queryFactory.select(member).from(member) + .where(member.tag.contains(keyword).or(member.profile.name.contains(keyword))).fetch(); + } +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/member/dto/response/MemberListResponse.java b/src/main/java/com/_119/wepro/member/dto/response/MemberListResponse.java new file mode 100644 index 0000000..122b1ae --- /dev/null +++ b/src/main/java/com/_119/wepro/member/dto/response/MemberListResponse.java @@ -0,0 +1,23 @@ +package com._119.wepro.member.dto.response; + +import com._119.wepro.member.domain.Member; +import java.io.Serializable; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class MemberListResponse implements Serializable { + + private Long id; + private String name; + private String tag; + + public static MemberListResponse of(Member member){ + return MemberListResponse.builder() + .id(member.getId()) + .name(member.getProfile().getName()) + .tag(member.getTag()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/member/presentation/MemberController.java b/src/main/java/com/_119/wepro/member/presentation/MemberController.java index a2293e1..9fa4e1c 100644 --- a/src/main/java/com/_119/wepro/member/presentation/MemberController.java +++ b/src/main/java/com/_119/wepro/member/presentation/MemberController.java @@ -1,9 +1,8 @@ package com._119.wepro.member.presentation; -import static com._119.wepro.global.security.constant.SecurityConstants.ACCESS_TOKEN_HEADER; -import static com._119.wepro.global.security.constant.SecurityConstants.REFRESH_TOKEN_HEADER; - import com._119.wepro.global.util.SecurityUtil; +import com._119.wepro.member.dto.response.MemberListResponse; +import com._119.wepro.member.service.MemberService; import com._119.wepro.member.service.ReissueService; import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletRequest; @@ -12,15 +11,17 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.List; @RestController @RequestMapping("members") @RequiredArgsConstructor public class MemberController { + private final MemberService memberService; private final SecurityUtil securityUtil; private final ReissueService reissueService; @@ -35,4 +36,10 @@ public ResponseEntity refresh(HttpServletRequest request, HttpServletRespo reissueService.reissue(request, response); return ResponseEntity.ok().build(); } + + @GetMapping() + public ResponseEntity> findMembers(@RequestParam("key") String keyword) { + List result = memberService.findMembers(keyword); + return ResponseEntity.ok(result); + } } diff --git a/src/main/java/com/_119/wepro/member/service/MemberService.java b/src/main/java/com/_119/wepro/member/service/MemberService.java index ab8c31b..608363c 100644 --- a/src/main/java/com/_119/wepro/member/service/MemberService.java +++ b/src/main/java/com/_119/wepro/member/service/MemberService.java @@ -1,10 +1,23 @@ package com._119.wepro.member.service; +import com._119.wepro.member.domain.Member; +import com._119.wepro.member.domain.repository.MemberCustomRepository; +import com._119.wepro.member.dto.response.MemberListResponse; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class MemberService { -} + private final MemberCustomRepository memberCustomRepository; + + @Cacheable(value = "findMemberCache") + public List findMembers(String keyword) { + List result = memberCustomRepository.findMembers(keyword); + + return result.stream().map(MemberListResponse::of).toList(); + } +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/project/domain/Project.java b/src/main/java/com/_119/wepro/project/domain/Project.java index e872de9..7e095ae 100644 --- a/src/main/java/com/_119/wepro/project/domain/Project.java +++ b/src/main/java/com/_119/wepro/project/domain/Project.java @@ -1,17 +1,71 @@ package com._119.wepro.project.domain; -import com._119.wepro.global.BaseEntity; +import com._119.wepro.image.domain.Image; +import com._119.wepro.project.dto.request.ProjectRequest.ProjectCreateRequest; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import lombok.Getter; +import jakarta.persistence.OneToMany; +import jakarta.persistence.PostPersist; +import java.time.LocalDate; +import java.util.List; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +@NoArgsConstructor +@AllArgsConstructor @Entity -@Getter -public class Project extends BaseEntity { +@Data +@Builder +public class Project { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; -} + + @Column(name = "name", length = 50, nullable = false) + private String name; + + @Column(name = "start_date", nullable = false) + private LocalDate startDate; + + @Column(name = "end_date", nullable = false) + private LocalDate endDate; + + @Column(name = "info", length = 255) + private String info; + + @Column(name = "member_num", nullable = false) + private int memberNum; + + @OneToMany(mappedBy = "project") + private Set projectMembers; + + private String tag; + + @OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true) + private List imgUrls; + + // 엔티티가 저장된 후 id로 태그를 생성합니다. + //todo 태그 저장안되는 이슈 확인하기 + @PostPersist + public void generateTag() { + this.tag = this.id.toString(); + } + + public static Project of(ProjectCreateRequest projectCreateRequest) { + return Project.builder() + .name(projectCreateRequest.getName()) + .startDate(projectCreateRequest.getStartDate()) + .endDate(projectCreateRequest.getEndDate()) + .info(projectCreateRequest.getDesc()) + .memberNum(0) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/project/domain/ProjectMember.java b/src/main/java/com/_119/wepro/project/domain/ProjectMember.java new file mode 100644 index 0000000..f108b64 --- /dev/null +++ b/src/main/java/com/_119/wepro/project/domain/ProjectMember.java @@ -0,0 +1,31 @@ +package com._119.wepro.project.domain; + +import com._119.wepro.member.domain.Member; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ProjectMember { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "project_id", nullable = false) + private Project project; + + @ManyToOne + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Column(nullable = false, length = 10) + private String role; +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/project/domain/ProjectMemberType.java b/src/main/java/com/_119/wepro/project/domain/ProjectMemberType.java new file mode 100644 index 0000000..86b8210 --- /dev/null +++ b/src/main/java/com/_119/wepro/project/domain/ProjectMemberType.java @@ -0,0 +1,6 @@ +package com._119.wepro.project.domain; + +public enum ProjectMemberType { + MEMBER, // 멤버 + TEAM_LEADER, // 팀장 +} diff --git a/src/main/java/com/_119/wepro/project/domain/repository/ProjectCustomRepository.java b/src/main/java/com/_119/wepro/project/domain/repository/ProjectCustomRepository.java new file mode 100644 index 0000000..23c28c4 --- /dev/null +++ b/src/main/java/com/_119/wepro/project/domain/repository/ProjectCustomRepository.java @@ -0,0 +1,21 @@ +package com._119.wepro.project.domain.repository; + +import static com._119.wepro.project.domain.QProject.project; + +import com._119.wepro.project.domain.Project; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class ProjectCustomRepository { + + private final JPAQueryFactory queryFactory; + + public List searchProjects(String keyword) { + return queryFactory.select(project).from(project) + .where(project.tag.contains(keyword).or(project.name.contains(keyword))).fetch(); + } +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/project/domain/repository/ProjectMemberCustomRepository.java b/src/main/java/com/_119/wepro/project/domain/repository/ProjectMemberCustomRepository.java new file mode 100644 index 0000000..3236eec --- /dev/null +++ b/src/main/java/com/_119/wepro/project/domain/repository/ProjectMemberCustomRepository.java @@ -0,0 +1,26 @@ +package com._119.wepro.project.domain.repository; + +import static com._119.wepro.project.domain.QProjectMember.projectMember; + +import com._119.wepro.member.domain.Member; +import com._119.wepro.project.domain.Project; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class ProjectMemberCustomRepository { + + private final JPAQueryFactory queryFactory; + + public Boolean existsByProjectAndMember(Project project, Member member) { + Integer fetchOne = queryFactory + .selectOne() + .from(projectMember) + .where(projectMember.project.eq(project).and( + projectMember.member.eq(member))).fetchFirst(); // limit 1 + + return fetchOne != null; + } +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/project/domain/repository/ProjectMemberRepository.java b/src/main/java/com/_119/wepro/project/domain/repository/ProjectMemberRepository.java new file mode 100644 index 0000000..6fc7dc9 --- /dev/null +++ b/src/main/java/com/_119/wepro/project/domain/repository/ProjectMemberRepository.java @@ -0,0 +1,8 @@ +package com._119.wepro.project.domain.repository; + +import com._119.wepro.project.domain.ProjectMember; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProjectMemberRepository extends JpaRepository { + +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/project/dto/request/ProjectMemberRequest.java b/src/main/java/com/_119/wepro/project/dto/request/ProjectMemberRequest.java new file mode 100644 index 0000000..09c07aa --- /dev/null +++ b/src/main/java/com/_119/wepro/project/dto/request/ProjectMemberRequest.java @@ -0,0 +1,10 @@ +package com._119.wepro.project.dto.request; + +import lombok.Data; + +public class ProjectMemberRequest { + @Data + public static class ProjectMemberCreateRequest { + private Long memberId; + } +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/project/dto/request/ProjectRequest.java b/src/main/java/com/_119/wepro/project/dto/request/ProjectRequest.java index e02bb77..1a820d7 100644 --- a/src/main/java/com/_119/wepro/project/dto/request/ProjectRequest.java +++ b/src/main/java/com/_119/wepro/project/dto/request/ProjectRequest.java @@ -1,8 +1,28 @@ package com._119.wepro.project.dto.request; +import java.time.LocalDate; +import java.util.List; +import lombok.Data; + public class ProjectRequest { - public static class ProjectDetailRequest { + @Data + public static class ProjectSearchCriteria { + String name; + String desc; + LocalDate startDate; + LocalDate endDate; + List memberTagList; + } + @Data + public static class ProjectCreateRequest { + private String name; + private String desc; + private List imgUrls; + private LocalDate startDate; + private LocalDate endDate; + private List memberList; + private String link; } -} +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/project/dto/response/ProjectDetailResponse.java b/src/main/java/com/_119/wepro/project/dto/response/ProjectDetailResponse.java new file mode 100644 index 0000000..ac83eb4 --- /dev/null +++ b/src/main/java/com/_119/wepro/project/dto/response/ProjectDetailResponse.java @@ -0,0 +1,43 @@ +package com._119.wepro.project.dto.response; + +import com._119.wepro.image.domain.Image; +import com._119.wepro.member.dto.response.MemberListResponse; +import com._119.wepro.project.domain.Project; +import java.time.LocalDate; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; + +@Data +@Builder +public class ProjectDetailResponse { + + private String name; + private String desc; + private List imgUrls; + private LocalDate startDate; + private LocalDate endDate; + private List memberList; + private List linkList; + + public static ProjectDetailResponse of(Project project) { + return ProjectDetailResponse.builder() + .name(project.getName()) + .desc(project.getInfo()) + .imgUrls(project.getImgUrls().stream().map(Image::getUrl).collect(Collectors.toList())) + .startDate(project.getStartDate()) + .endDate(project.getEndDate()) + .memberList( + project.getProjectMembers().stream() + .map(projectMember -> MemberListResponse.of(projectMember.getMember())) + .collect(Collectors.toList()) + ) + .linkList(Collections.emptyList()) + .build(); + + } +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/project/dto/response/ProjectListResponse.java b/src/main/java/com/_119/wepro/project/dto/response/ProjectListResponse.java new file mode 100644 index 0000000..c9fb212 --- /dev/null +++ b/src/main/java/com/_119/wepro/project/dto/response/ProjectListResponse.java @@ -0,0 +1,21 @@ +package com._119.wepro.project.dto.response; + +import com._119.wepro.project.domain.Project; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ProjectListResponse { + private String name; + private Integer memberNum; + private String imgUrl; + + public static ProjectListResponse of(Project project) { + return ProjectListResponse.builder() + .name(project.getName()) + .memberNum(project.getMemberNum()) + .imgUrl("project img Url") + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/project/dto/response/ProjectResponse.java b/src/main/java/com/_119/wepro/project/dto/response/ProjectResponse.java deleted file mode 100644 index 2898100..0000000 --- a/src/main/java/com/_119/wepro/project/dto/response/ProjectResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package com._119.wepro.project.dto.response; - -public class ProjectResponse { - -} diff --git a/src/main/java/com/_119/wepro/project/presentation/ProjectController.java b/src/main/java/com/_119/wepro/project/presentation/ProjectController.java index 3c0c64a..b093e36 100644 --- a/src/main/java/com/_119/wepro/project/presentation/ProjectController.java +++ b/src/main/java/com/_119/wepro/project/presentation/ProjectController.java @@ -1,16 +1,76 @@ package com._119.wepro.project.presentation; +import com._119.wepro.global.security.CustomOidcUser; +import com._119.wepro.global.util.SecurityUtil; +import com._119.wepro.project.dto.request.ProjectMemberRequest.ProjectMemberCreateRequest; +import com._119.wepro.project.dto.request.ProjectRequest.ProjectCreateRequest; +import com._119.wepro.project.dto.response.ProjectDetailResponse; +import com._119.wepro.project.dto.response.ProjectListResponse; +import com._119.wepro.project.service.ProjectService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController +@RequiredArgsConstructor @RequestMapping("/projects") +@Slf4j public class ProjectController { + private final ProjectService projectService; + private final SecurityUtil securityUtil; + @GetMapping() - public ResponseEntity getProjects() { - return ResponseEntity.ok("Hello World"); + public ResponseEntity> searchProjects( + @RequestParam("key") String keyword) { + List result = projectService.searchProjects(keyword); + return ResponseEntity.ok(result); + } + + @GetMapping("/{id}") + public ResponseEntity getProjectDetail(@PathVariable("id") Long id) { + ProjectDetailResponse result = projectService.getProjectDetail(id); + return ResponseEntity.ok(result); + } + + @PostMapping() + public ResponseEntity createProject( + @RequestBody ProjectCreateRequest projectCreateRequest + ) { + projectService.createProject(projectCreateRequest, securityUtil.getCurrentMemberId()); + return ResponseEntity.ok(null); + } + + @PutMapping() + public ResponseEntity updateProject() { + //TODO + return null; + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteProject(@PathVariable Long id) { + return ResponseEntity.ok(projectService.deleteProject(id)); + } + + @PostMapping("/{id}/member") + public ResponseEntity addMember(@RequestBody ProjectMemberCreateRequest dto, + @PathVariable("id") Long id) { + projectService.addProjectMember(dto.getMemberId(), id); + return ResponseEntity.ok(null); } -} +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/project/service/ProjectService.java b/src/main/java/com/_119/wepro/project/service/ProjectService.java index 41d7a51..81ab73a 100644 --- a/src/main/java/com/_119/wepro/project/service/ProjectService.java +++ b/src/main/java/com/_119/wepro/project/service/ProjectService.java @@ -1,11 +1,117 @@ package com._119.wepro.project.service; +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.RESOURCE_NOT_FOUND; +import static com._119.wepro.global.exception.errorcode.ProjectErrorCode.PROJECT_MEMBER_NOT_FOUND; +import static com._119.wepro.project.domain.ProjectMemberType.MEMBER; +import static com._119.wepro.project.domain.ProjectMemberType.TEAM_LEADER; + +import com._119.wepro.global.exception.RestApiException; +import com._119.wepro.member.domain.Member; +import com._119.wepro.member.domain.repository.MemberRepository; +import com._119.wepro.project.domain.Project; +import com._119.wepro.project.domain.ProjectMember; +import com._119.wepro.project.domain.repository.ProjectCustomRepository; +import com._119.wepro.project.domain.repository.ProjectMemberCustomRepository; +import com._119.wepro.project.domain.repository.ProjectMemberRepository; +import com._119.wepro.project.domain.repository.ProjectRepository; +import com._119.wepro.project.dto.request.ProjectRequest.ProjectCreateRequest; +import com._119.wepro.project.dto.response.ProjectDetailResponse; +import com._119.wepro.project.dto.response.ProjectListResponse; +import jakarta.persistence.EntityNotFoundException; +import jakarta.transaction.Transactional; +import java.util.List; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class ProjectService { - public void retrieveProjects() { - // TODO Not Yet Implemented + private final ProjectRepository projectRepository; + private final ProjectMemberRepository projectMemberRepository; + private final ProjectMemberCustomRepository projectMemberCustomRepository; + private final MemberRepository memberRepository; + private final ProjectCustomRepository projectCustomRepository; + + public List searchProjects(String keyword) { + List result = projectCustomRepository.searchProjects(keyword); + + return result.stream().map(ProjectListResponse::of).toList(); + } + + public ProjectDetailResponse getProjectDetail(Long projectId) { + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> new RestApiException(RESOURCE_NOT_FOUND)); + + return ProjectDetailResponse.of(project); + } + + @Transactional + public void createProject(ProjectCreateRequest projectCreateRequest, Long projectCreatorId) { + Project newProject = Project.of(projectCreateRequest); + projectRepository.save(newProject); + + // 팀원 멤버 역할로 등록 + for (Long memberId : projectCreateRequest.getMemberList()) { + registerProjectMember(newProject, memberId, MEMBER.name()); + } + + // 팀장 등록 + registerProjectMember(newProject, projectCreatorId, TEAM_LEADER.name()); + + projectRepository.save(newProject); + } + + private void registerProjectMember(Project project, Long memberId, String role) { + // findById를 사용하여 실제 member가 존재하는지 확인 + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new RestApiException(PROJECT_MEMBER_NOT_FOUND)); + + ProjectMember projectMember = ProjectMember.builder() + .project(project) + .member(member) + .role(role) + .build(); + + projectMemberRepository.save(projectMember); + + // 멤버 수 증가 + project.setMemberNum(project.getMemberNum() + 1); + } + + + public Long deleteProject(Long projectId) { + Project project = projectRepository.findById(projectId).orElseThrow(() -> new RestApiException( + RESOURCE_NOT_FOUND)); + projectRepository.delete(project); + + return project.getId(); + } + + @Transactional + public void addProjectMember(Long projectId, Long userId) { + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> new RestApiException(RESOURCE_NOT_FOUND)); + + //todo : 탈퇴한 멤버 제외 + Member member = memberRepository.findById(userId) + .orElseThrow(() -> new RestApiException(RESOURCE_NOT_FOUND)); + + // 기존에 해당 프로젝트와 멤버 조합이 있는지 확인합니다. + boolean exists = projectMemberCustomRepository.existsByProjectAndMember(project, member); + if (exists) { + throw new IllegalArgumentException("This member is already part of the project."); + } + + ProjectMember projectMember = ProjectMember.builder() + .project(project) + .member(member) + .role("member") + .build(); + + projectMemberRepository.save(projectMember); + + project.setMemberNum(project.getMemberNum() + 1); + projectRepository.save(project); } -} +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 029a2d3..c574c36 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -15,6 +15,19 @@ spring: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect + servlet: + multipart: + maxFileSize: 10MB # 파일 하나의 최대 크기 + maxRequestSize: 30MB # 한 번에 최대 업로드 가능 용량 + + thymeleaf: + cache: false + + data: + redis: + host: localhost + port: 6379 + security: debug: true oauth2: @@ -36,14 +49,17 @@ spring: user-name-attribute: sub jwk-set-uri: https://kauth.kakao.com/.well-known/jwks.json - data: - redis: - host: localhost - port: 6379 + jwt: secret: ${jwt.secret} +logging: + level: + org.springframework.web.client.RestTemplate: DEBUG + org.springframework.security: DEBUG + org.springframework.security.oauth2.client: DEBUG + org.springframework.security.oauth2.core: DEBUG app: cors: @@ -58,8 +74,8 @@ server: cloud: aws: credentials: - accessKey: ${aws.credentials.accessKey} - secretKey: ${aws.credentials.secretKey} + accessKey: ${aws.accessKey} + secretKey: ${aws.secretKey} s3: bucket: wepro1 folder: profile/