diff --git a/src/main/java/com/gongjakso/server/domain/apply/controller/ApplyController.java b/src/main/java/com/gongjakso/server/domain/apply/controller/ApplyController.java new file mode 100644 index 00000000..1c498a13 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/apply/controller/ApplyController.java @@ -0,0 +1,66 @@ +package com.gongjakso.server.domain.apply.controller; + +import com.gongjakso.server.domain.apply.service.ApplyService; +import com.gongjakso.server.domain.apply.dto.request.ApplyReq; +import com.gongjakso.server.domain.apply.dto.response.ApplyRes; +import com.gongjakso.server.domain.apply.dto.request.StatusReq; +import com.gongjakso.server.global.common.ApplicationResponse; +import com.gongjakso.server.global.security.PrincipalDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequestMapping("/api/v2/apply") +@RequiredArgsConstructor +@Tag(name = "Apply", description = "지원 관련 API") +public class ApplyController { + + private final ApplyService applyService; + + @Operation(summary = "지원하기", description = "팀에 지원하는 API") + @PostMapping("/{team_id}") + public ApplicationResponse apply(@AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable("team_id") Long teamId, + @Valid @RequestBody ApplyReq req) { + return ApplicationResponse.ok(applyService.apply(principalDetails.getMember(), teamId, req)); + } + + @Operation(summary = "내가 지원한 팀 조회", description = "내가 지원한 팀을 페이징 조회하는 API") + @GetMapping("/my") + public ApplicationResponse> getMyApplies(@AuthenticationPrincipal PrincipalDetails principalDetails, + @RequestParam Long page) { + Pageable pageable = PageRequest.of(page.intValue(), 6); + return ApplicationResponse.ok(applyService.getMyApplies(principalDetails.getMember(), pageable)); + } + + @Operation(summary = "특정 지원자 지원서 열람하기", description = "특정 지원자의 지원서를 열람하는 API") + @GetMapping("/{apply_id}") + public ApplicationResponse getApply(@AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable("apply_id") Long applyId) { + return ApplicationResponse.ok(applyService.getApply(principalDetails.getMember(), applyId)); + } + + @Operation(summary = "지원서 선발/미선발", description = "지원자를 선발/미선발하는 API") + @PatchMapping("/select/{apply_id}") + public ApplicationResponse selectApply(@AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable("apply_id") Long applyId, + @Valid @RequestBody StatusReq req) { + return ApplicationResponse.ok(applyService.selectApply(principalDetails.getMember(), applyId, req)); + } + + @Operation(summary = "지원 취소", description = "지원자가 지원 취소하는 API") + @DeleteMapping("/{apply_id}") + public ApplicationResponse cancelApply(@AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable("apply_id") Long applyId) { + applyService.cancelApply(principalDetails.getMember(), applyId); + return ApplicationResponse.ok(); + } +} diff --git a/src/main/java/com/gongjakso/server/domain/apply/dto/request/ApplyReq.java b/src/main/java/com/gongjakso/server/domain/apply/dto/request/ApplyReq.java new file mode 100644 index 00000000..0cdce274 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/apply/dto/request/ApplyReq.java @@ -0,0 +1,56 @@ +package com.gongjakso.server.domain.apply.dto.request; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.gongjakso.server.domain.member.entity.Member; +import com.gongjakso.server.domain.portfolio.entity.Portfolio; +import com.gongjakso.server.domain.apply.entity.Apply; +import com.gongjakso.server.domain.apply.entity.PortfolioInfo; +import com.gongjakso.server.domain.team.entity.Team; +import com.gongjakso.server.domain.apply.enumerate.ApplyStatus; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Builder +public record ApplyReq( + + @Nullable + @Schema(description = "포트폴리오 ID", example = "1") + Long portfolioId, + + @NotNull + @Schema(description = "포트폴리오 공개 설정", example = "false") + Boolean isPrivate, + + @Size(max = 500) + @Schema(description = "지원 이유", example = "저는 데이터 사이언스 과목을 수강하며 데이터에 관한 기본적인 내용들을 배우며 이런 데이터를 잘 활용하고, 이용하는 것이 중요한 역량이 될 것 같다고 판단했습니다. 그래서 관련된 역량을 쌓고자 공모전에 출품하고 싶다는 생각을 가지게 되었고, 공공데이터 공모전이 적합하다고 생각했습니다!") + String body, + + @Size(max = 20) + @NotNull + @Schema(description = "지원 상태", example = "COMPLETED") + ApplyStatus status, + + @Size(max = 20) + @NotNull + @Schema(description = "지원 파트", example = "기획") + String part +) { + public static Apply toEntity(ApplyReq req, Team team, Member member, @Nullable Portfolio portfolio) { + PortfolioInfo portfolioInfo = portfolio != null + ? PortfolioInfo.ofPortfolio(portfolio) + : PortfolioInfo.ofPrivate(); + + return Apply.builder() + .team(team) + .member(member) + .portfolioInfo(portfolioInfo) + .body(req.body()) + .status(req.status()) + .part(req.part()) + .build(); + } +} diff --git a/src/main/java/com/gongjakso/server/domain/apply/dto/request/StatusReq.java b/src/main/java/com/gongjakso/server/domain/apply/dto/request/StatusReq.java new file mode 100644 index 00000000..0494f788 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/apply/dto/request/StatusReq.java @@ -0,0 +1,19 @@ +package com.gongjakso.server.domain.apply.dto.request; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.gongjakso.server.domain.apply.enumerate.ApplyStatus; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Builder +public record StatusReq( + + @Size(max = 20) + @NotNull + @Schema(description = "지원 상태", example = "SELECTED") + ApplyStatus status +) { +} diff --git a/src/main/java/com/gongjakso/server/domain/apply/dto/response/ApplyRes.java b/src/main/java/com/gongjakso/server/domain/apply/dto/response/ApplyRes.java new file mode 100644 index 00000000..ad84b317 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/apply/dto/response/ApplyRes.java @@ -0,0 +1,81 @@ +package com.gongjakso.server.domain.apply.dto.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.gongjakso.server.domain.apply.entity.Apply; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Period; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Builder +public record ApplyRes( + + @NotNull + Long applyId, + + @NotNull + Long teamId, + + @NotNull + Long memberId, + + @Nullable + Long portfolioId, + + @Nullable + String portfolioTitle, + + @Size(max = 500) + String body, + + @Size(max = 20) + @NotNull + String status, + + @Size(max = 20) + @NotNull + String part, + + int scrapCount, + + int remainingDays, + + LocalDate startedAt, + + LocalDate finishedAt, + + String teamName, + + String leaderName, + + Boolean isViewed, + + LocalDateTime deleteAt + +) { + public static ApplyRes of(Apply apply) { + return ApplyRes.builder() + .applyId(apply.getId()) + .teamId(apply.getTeam().getId()) + .teamName(apply.getTeam().getTitle()) + .memberId(apply.getMember().getId()) + .leaderName(apply.getTeam().getMember().getName()) + .portfolioId(apply.getPortfolioInfo().getPortfolio() != null ? apply.getPortfolioInfo().getPortfolio().getId() : null) + .portfolioTitle(apply.getPortfolioInfo().getPortfolio() != null ? apply.getPortfolioInfo().getPortfolio().getTitle() : null) + .body(apply.getBody()) + .status(apply.getStatus().getDescription()) + .part(apply.getPart()) + .isViewed(apply.isViewed()) + .deleteAt(apply.getDeletedAt()) + .scrapCount(apply.getTeam().getScrapCount()) + .startedAt(apply.getTeam().getStartedAt()) + .finishedAt(apply.getTeam().getFinishedAt()) + .remainingDays(Period.between(LocalDate.now(), apply.getTeam().getFinishedAt()).getDays()) + .build(); + } +} diff --git a/src/main/java/com/gongjakso/server/domain/apply/entity/Apply.java b/src/main/java/com/gongjakso/server/domain/apply/entity/Apply.java new file mode 100644 index 00000000..ddfa6b0f --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/apply/entity/Apply.java @@ -0,0 +1,68 @@ +package com.gongjakso.server.domain.apply.entity; + +import com.gongjakso.server.domain.member.entity.Member; +import com.gongjakso.server.domain.team.entity.Team; +import com.gongjakso.server.domain.apply.enumerate.ApplyStatus; +import com.gongjakso.server.global.common.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; + +@Getter +@Entity +@Table(name = "apply") +@SQLDelete(sql = "UPDATE apply SET deleted_at = NOW() where id = ?") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Apply extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false, columnDefinition = "bigint") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team_id", nullable = false, columnDefinition = "bigint") + private Team team; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="member_id", nullable = false, columnDefinition = "bigint") + private Member member; + + @Embedded + private PortfolioInfo portfolioInfo; + + @Column(nullable = false, columnDefinition = "varchar(500)") + private String body; + + @Column(nullable = false, columnDefinition = "varchar(20)") + @Enumerated(EnumType.STRING) + private ApplyStatus status; + + @Column(nullable = false, columnDefinition = "varchar(20)") + private String part; + + @Column(nullable = false, columnDefinition = "tinyint(1) default 0") + private boolean isViewed; + + @Builder + public Apply(Team team, Member member, PortfolioInfo portfolioInfo, String body, ApplyStatus status, String part) { + this.team = team; + this.member = member; + this.portfolioInfo = portfolioInfo; + this.body = body; + this.status = status; + this.part = part; + this.isViewed = false; + } + + public void select(ApplyStatus status) { + this.status = status; + } + + public void setViewed() { + this.isViewed = true; + } +} diff --git a/src/main/java/com/gongjakso/server/domain/apply/entity/PortfolioInfo.java b/src/main/java/com/gongjakso/server/domain/apply/entity/PortfolioInfo.java new file mode 100644 index 00000000..c6cb7a01 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/apply/entity/PortfolioInfo.java @@ -0,0 +1,36 @@ +package com.gongjakso.server.domain.apply.entity; + +import com.gongjakso.server.domain.portfolio.entity.Portfolio; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PortfolioInfo { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "portfolio_id", columnDefinition = "bigint") + private Portfolio portfolio; + + private boolean isPrivate; + + public static PortfolioInfo ofPortfolio(Portfolio portfolio) { + return PortfolioInfo.builder() + .portfolio(portfolio) + .isPrivate(false) + .build(); + } + + public static PortfolioInfo ofPrivate() { + return PortfolioInfo.builder() + .portfolio(null) + .isPrivate(true) + .build(); + } +} diff --git a/src/main/java/com/gongjakso/server/domain/apply/enumerate/ApplyStatus.java b/src/main/java/com/gongjakso/server/domain/apply/enumerate/ApplyStatus.java new file mode 100644 index 00000000..f69a6bf3 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/apply/enumerate/ApplyStatus.java @@ -0,0 +1,14 @@ +package com.gongjakso.server.domain.apply.enumerate; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ApplyStatus { + COMPLETED("지원 완료"), + ACCEPTED("합격"), + REJECTED("불합격"); + + private final String description; +} diff --git a/src/main/java/com/gongjakso/server/domain/apply/repository/ApplyRepository.java b/src/main/java/com/gongjakso/server/domain/apply/repository/ApplyRepository.java new file mode 100644 index 00000000..b85d4fa0 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/apply/repository/ApplyRepository.java @@ -0,0 +1,11 @@ +package com.gongjakso.server.domain.apply.repository; + +import com.gongjakso.server.domain.apply.entity.Apply; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ApplyRepository extends JpaRepository, ApplyRepositoryCustom { + Boolean existsByMemberIdAndTeamIdAndDeletedAtIsNull(Long memberId, Long teamId); + Optional findByIdAndDeletedAtIsNull(Long applyId); +} diff --git a/src/main/java/com/gongjakso/server/domain/apply/repository/ApplyRepositoryCustom.java b/src/main/java/com/gongjakso/server/domain/apply/repository/ApplyRepositoryCustom.java new file mode 100644 index 00000000..d80230c7 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/apply/repository/ApplyRepositoryCustom.java @@ -0,0 +1,14 @@ +package com.gongjakso.server.domain.apply.repository; + +import com.gongjakso.server.domain.member.entity.Member; +import com.gongjakso.server.domain.apply.dto.response.ApplyRes; +import com.gongjakso.server.domain.apply.entity.Apply; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +public interface ApplyRepositoryCustom { + Page findByMemberAndPage(Member member, Pageable pageable); + Optional findApplyDetails(Long applyId); +} diff --git a/src/main/java/com/gongjakso/server/domain/apply/repository/ApplyRepositoryImpl.java b/src/main/java/com/gongjakso/server/domain/apply/repository/ApplyRepositoryImpl.java new file mode 100644 index 00000000..ede378e2 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/apply/repository/ApplyRepositoryImpl.java @@ -0,0 +1,75 @@ +package com.gongjakso.server.domain.apply.repository; + +import com.gongjakso.server.domain.member.entity.Member; +import com.gongjakso.server.domain.apply.dto.response.ApplyRes; +import com.gongjakso.server.domain.apply.entity.Apply; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; + +import static com.gongjakso.server.domain.apply.entity.QApply.apply; + +@RequiredArgsConstructor +public class ApplyRepositoryImpl implements ApplyRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + public Page findByMemberAndPage(Member member, Pageable pageable) { + List applyList = queryFactory + .selectFrom(apply) + .leftJoin(apply.team).fetchJoin() + .leftJoin(apply.member).fetchJoin() + .leftJoin(apply.portfolioInfo.portfolio).fetchJoin() + .leftJoin(apply.team.member).fetchJoin() + .where(apply.member.eq(member), + apply.deletedAt.isNull(), + apply.team.deletedAt.isNull(), + apply.member.deletedAt.isNull(), + apply.team.member.deletedAt.isNull(), + apply.portfolioInfo.portfolio.deletedAt.isNull()) + .orderBy(apply.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + List content = applyList.stream() + .map(ApplyRes::of) + .toList(); + + Long total = queryFactory + .select(apply.count()) + .from(apply) + .where(apply.member.eq(member), + apply.deletedAt.isNull(), + apply.team.deletedAt.isNull(), + apply.member.deletedAt.isNull(), + apply.team.member.deletedAt.isNull(), + apply.portfolioInfo.portfolio.deletedAt.isNull()) + .fetchOne(); + + return new PageImpl<>(content, pageable, total != null ? total : 0L); + } + + public Optional findApplyDetails(Long applyId) { + return Optional.ofNullable(queryFactory + .select(apply) + .from(apply) + .leftJoin(apply.member).fetchJoin() + .leftJoin(apply.team).fetchJoin() + .leftJoin(apply.team.member).fetchJoin() + .leftJoin(apply.portfolioInfo.portfolio).fetchJoin() + .where(apply.id.eq(applyId), + apply.deletedAt.isNull(), + apply.team.deletedAt.isNull(), + apply.member.deletedAt.isNull(), + apply.team.member.deletedAt.isNull(), + apply.portfolioInfo.portfolio.deletedAt.isNull()) + .fetchOne()); + } + +} \ No newline at end of file diff --git a/src/main/java/com/gongjakso/server/domain/apply/service/ApplyService.java b/src/main/java/com/gongjakso/server/domain/apply/service/ApplyService.java new file mode 100644 index 00000000..41ae3ef3 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/apply/service/ApplyService.java @@ -0,0 +1,129 @@ +package com.gongjakso.server.domain.apply.service; + +import com.gongjakso.server.domain.member.entity.Member; +import com.gongjakso.server.domain.portfolio.entity.Portfolio; +import com.gongjakso.server.domain.portfolio.repository.PortfolioRepository; +import com.gongjakso.server.domain.apply.dto.request.ApplyReq; +import com.gongjakso.server.domain.apply.dto.response.ApplyRes; +import com.gongjakso.server.domain.apply.dto.request.StatusReq; +import com.gongjakso.server.domain.apply.entity.Apply; +import com.gongjakso.server.domain.team.entity.Team; +import com.gongjakso.server.domain.apply.repository.ApplyRepository; +import com.gongjakso.server.domain.team.repository.TeamRepository; +import com.gongjakso.server.global.exception.ApplicationException; +import com.gongjakso.server.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.time.chrono.ChronoLocalDate; + +import static java.time.LocalDateTime.now; + +@Service +@RequiredArgsConstructor +public class ApplyService { + + private final ApplyRepository applyRepository; + private final PortfolioRepository portfolioRepository; + private final TeamRepository teamRepository; + + public ApplyRes apply(Member member, Long teamId, ApplyReq applyReq) { + //Validation: teamId, 지원 가능 기간인지, 리더가 지원하는지, 이미 지원했는지, 본인의 포트폴리오인지 유효성 검사 + Team team = teamRepository.findTeamById(teamId) + .orElseThrow(() -> new ApplicationException(ErrorCode.TEAM_NOT_FOUND_EXCEPTION)); + + if(team.getFinishedAt().isBefore(ChronoLocalDate.from(now()))) { + throw new ApplicationException(ErrorCode.APPLY_PERIOD_EXPIRED_EXCEPTION); + } + + if(team.getMember().equals(member)) { + throw new ApplicationException(ErrorCode.APPLY_LEADER_NOT_ALLOWED_EXCEPTION); + } + + if(applyRepository.existsByMemberIdAndTeamIdAndDeletedAtIsNull(member.getId(), teamId)) { + throw new ApplicationException(ErrorCode.APPLY_ALREADY_EXISTS_EXCEPTION); + } + + //Business Logic + Portfolio portfolio = null; + + if (!applyReq.isPrivate()) { + if (applyReq.portfolioId() == null) { + throw new ApplicationException(ErrorCode.PORTFOLIO_NOT_FOUND_EXCEPTION); + } + portfolio = portfolioRepository.findById(applyReq.portfolioId()) + .orElseThrow(() -> new ApplicationException(ErrorCode.PORTFOLIO_NOT_FOUND_EXCEPTION)); + if(!portfolio.getMember().getId().equals(member.getId())) { + throw new ApplicationException(ErrorCode.PORTFOLIO_UNAUTHORIZED_EXCEPTION); + } + } + + Apply apply = ApplyReq.toEntity(applyReq, team, member, portfolio); + + applyRepository.save(apply); + + //Response + return ApplyRes.of(apply); + } + + public Page getMyApplies(Member member, Pageable pageable) { + //Business Logic + Page applyPage = applyRepository.findByMemberAndPage(member, pageable); + + //Response + return applyPage; + } + + public ApplyRes getApply(Member member, Long applyId) { + //Validation: Apply가 유효하지 않거나, 리더가 아니거나, 자신이 아닌 경우 예외 처리 + Apply apply = applyRepository.findApplyDetails(applyId) + .orElseThrow(() -> new ApplicationException(ErrorCode.APPLY_NOT_FOUND_EXCEPTION)); + + if (!member.getId().equals(apply.getTeam().getMember().getId()) + && !member.getId().equals(apply.getMember().getId())) { + throw new ApplicationException(ErrorCode.UNAUTHORIZED_EXCEPTION); + } + + if(!apply.isViewed() && member.getId().equals(apply.getTeam().getMember().getId())) { + apply.setViewed(); + applyRepository.save(apply); + } + + //Response + return ApplyRes.of(apply); + } + + public ApplyRes selectApply(Member member, Long applyId, StatusReq req){ + //Validation: Apply가 유효하지 않거나, 리더가 아닌 경우 예외 처리 + Apply apply = applyRepository.findApplyDetails(applyId).orElseThrow(() -> new ApplicationException(ErrorCode.APPLY_NOT_FOUND_EXCEPTION)); + + if(!member.getId().equals(apply.getTeam().getMember().getId())){ + throw new ApplicationException(ErrorCode.UNAUTHORIZED_EXCEPTION); + } + + //Business Logic + if (req != null) { + apply.select(req.status()); + } + + applyRepository.save(apply); + + //Response + return ApplyRes.of(apply); + } + + public void cancelApply(Member member, Long applyId) { + //Validation: Apply가 유효하지 않거나, 지원자 아닌 경우 예외 처리 + Apply apply = applyRepository.findByIdAndDeletedAtIsNull(applyId) + .orElseThrow(() -> new ApplicationException(ErrorCode.APPLY_NOT_FOUND_EXCEPTION)); + + if(!member.getId().equals(apply.getMember().getId())){ + throw new ApplicationException(ErrorCode.UNAUTHORIZED_EXCEPTION); + } + + //Business Logic + applyRepository.delete(apply); + } +} diff --git a/src/main/java/com/gongjakso/server/domain/portfolio/controller/PortfolioController.java b/src/main/java/com/gongjakso/server/domain/portfolio/controller/PortfolioController.java index 20c4ba1b..3b1d1f57 100644 --- a/src/main/java/com/gongjakso/server/domain/portfolio/controller/PortfolioController.java +++ b/src/main/java/com/gongjakso/server/domain/portfolio/controller/PortfolioController.java @@ -2,10 +2,12 @@ import com.gongjakso.server.domain.portfolio.dto.request.PortfolioReq; import com.gongjakso.server.domain.portfolio.dto.response.PortfolioRes; +import com.gongjakso.server.domain.portfolio.dto.response.SimplePortfolioRes; import com.gongjakso.server.domain.portfolio.service.PortfolioService; import com.gongjakso.server.global.common.ApplicationResponse; import com.gongjakso.server.global.security.PrincipalDetails; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -18,9 +20,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @RestController @RequiredArgsConstructor @RequestMapping("api/v2/mypage/portfolio") +@Tag(name = "Portfolio", description = "포트폴리오 관련 API") public class PortfolioController { private final PortfolioService portfolioService; @@ -55,4 +60,10 @@ public ApplicationResponse deletePortfolio(@AuthenticationPrincipal Princi return ApplicationResponse.ok(); } + + @Operation(description = "내 포트폴리오 리스트 조회 API") + @GetMapping("/my") + public ApplicationResponse> getMyPortfolios(@AuthenticationPrincipal PrincipalDetails principalDetails) { + return ApplicationResponse.ok(portfolioService.getMyPortfolios(principalDetails.getMember())); + } } diff --git a/src/main/java/com/gongjakso/server/domain/portfolio/dto/response/SimplePortfolioRes.java b/src/main/java/com/gongjakso/server/domain/portfolio/dto/response/SimplePortfolioRes.java new file mode 100644 index 00000000..d6daf3b5 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/portfolio/dto/response/SimplePortfolioRes.java @@ -0,0 +1,19 @@ +package com.gongjakso.server.domain.portfolio.dto.response; + +import com.gongjakso.server.domain.portfolio.entity.Portfolio; + +import java.time.LocalDateTime; + +public record SimplePortfolioRes( + Long PortfolioId, + String PortfolioTitle, + LocalDateTime modifiedAt +) { + public static SimplePortfolioRes of(Portfolio portfolio) { + return new SimplePortfolioRes( + portfolio.getId(), + portfolio.getTitle(), + portfolio.getModifiedAt() + ); + } +} diff --git a/src/main/java/com/gongjakso/server/domain/portfolio/entity/Portfolio.java b/src/main/java/com/gongjakso/server/domain/portfolio/entity/Portfolio.java index 9de2a751..0e3e2338 100644 --- a/src/main/java/com/gongjakso/server/domain/portfolio/entity/Portfolio.java +++ b/src/main/java/com/gongjakso/server/domain/portfolio/entity/Portfolio.java @@ -25,6 +25,9 @@ public class Portfolio extends BaseTimeEntity { @Column(name = "portfolio_id", nullable = false, columnDefinition = "bigint") private Long id; + @Column(name = "title", nullable = false, columnDefinition = "varchar(50)") + private String title; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; diff --git a/src/main/java/com/gongjakso/server/domain/portfolio/repository/PortfolioRepository.java b/src/main/java/com/gongjakso/server/domain/portfolio/repository/PortfolioRepository.java index d4bc90fc..cb0cc04b 100644 --- a/src/main/java/com/gongjakso/server/domain/portfolio/repository/PortfolioRepository.java +++ b/src/main/java/com/gongjakso/server/domain/portfolio/repository/PortfolioRepository.java @@ -4,7 +4,7 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -public interface PortfolioRepository extends JpaRepository { +public interface PortfolioRepository extends JpaRepository, PortfolioRepositoryCustom{ Optional findByIdAndDeletedAtIsNull(Long portfolioId); long countByDeletedAtIsNull(); diff --git a/src/main/java/com/gongjakso/server/domain/portfolio/repository/PortfolioRepositoryCustom.java b/src/main/java/com/gongjakso/server/domain/portfolio/repository/PortfolioRepositoryCustom.java new file mode 100644 index 00000000..d7ddf394 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/portfolio/repository/PortfolioRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.gongjakso.server.domain.portfolio.repository; + +import com.gongjakso.server.domain.member.entity.Member; +import com.gongjakso.server.domain.portfolio.entity.Portfolio; + +import java.util.List; + +public interface PortfolioRepositoryCustom { + List findByMemberAndDeletedAtIsNull(Member member); +} diff --git a/src/main/java/com/gongjakso/server/domain/portfolio/repository/PortfolioRepositoryImpl.java b/src/main/java/com/gongjakso/server/domain/portfolio/repository/PortfolioRepositoryImpl.java new file mode 100644 index 00000000..df152c6b --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/portfolio/repository/PortfolioRepositoryImpl.java @@ -0,0 +1,23 @@ +package com.gongjakso.server.domain.portfolio.repository; + +import com.gongjakso.server.domain.member.entity.Member; +import com.gongjakso.server.domain.portfolio.entity.Portfolio; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +import static com.gongjakso.server.domain.portfolio.entity.QPortfolio.portfolio; + +@RequiredArgsConstructor +public class PortfolioRepositoryImpl { + private final JPAQueryFactory queryFactory; + + public List findByMemberAndDeletedAtIsNull(Member member) { + return queryFactory + .selectFrom(portfolio) + .where(portfolio.member.eq(member) + .and(portfolio.deletedAt.isNull())) + .fetch(); + } +} diff --git a/src/main/java/com/gongjakso/server/domain/portfolio/service/PortfolioService.java b/src/main/java/com/gongjakso/server/domain/portfolio/service/PortfolioService.java index f2e64faf..b1704746 100644 --- a/src/main/java/com/gongjakso/server/domain/portfolio/service/PortfolioService.java +++ b/src/main/java/com/gongjakso/server/domain/portfolio/service/PortfolioService.java @@ -3,6 +3,7 @@ import com.gongjakso.server.domain.member.entity.Member; import com.gongjakso.server.domain.portfolio.dto.request.PortfolioReq; import com.gongjakso.server.domain.portfolio.dto.response.PortfolioRes; +import com.gongjakso.server.domain.portfolio.dto.response.SimplePortfolioRes; import com.gongjakso.server.domain.portfolio.entity.Portfolio; import com.gongjakso.server.domain.portfolio.vo.PortfolioData; import com.gongjakso.server.domain.portfolio.repository.PortfolioRepository; @@ -142,4 +143,11 @@ public void deletePortfolio(Member member, Long portfolioId) { } portfolioRepository.delete(portfolio); } + + public List getMyPortfolios(Member member) { + List portfolioList = portfolioRepository.findByMemberAndDeletedAtIsNull(member); + return portfolioList.stream() + .map(SimplePortfolioRes::of) + .toList(); + } } \ No newline at end of file diff --git a/src/main/java/com/gongjakso/server/domain/team/entity/Apply.java b/src/main/java/com/gongjakso/server/domain/team/entity/Apply.java deleted file mode 100644 index fa9b731e..00000000 --- a/src/main/java/com/gongjakso/server/domain/team/entity/Apply.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.gongjakso.server.domain.team.entity; - -import com.gongjakso.server.global.common.BaseTimeEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.hibernate.annotations.SQLDelete; - -@Getter -@Entity -@Table(name = "apply") -@SQLDelete(sql = "UPDATE apply SET deleted_at = NOW() where id = ?") -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Apply extends BaseTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", nullable = false, columnDefinition = "bigint") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "team_id", nullable = false, columnDefinition = "bigint") - private Team team; - - - @Builder - public Apply(Team team){ - this.team = team; - } -} diff --git a/src/main/java/com/gongjakso/server/domain/team/repository/TeamRepositoryCustom.java b/src/main/java/com/gongjakso/server/domain/team/repository/TeamRepositoryCustom.java index eb7b28f5..1692edbf 100644 --- a/src/main/java/com/gongjakso/server/domain/team/repository/TeamRepositoryCustom.java +++ b/src/main/java/com/gongjakso/server/domain/team/repository/TeamRepositoryCustom.java @@ -1,11 +1,16 @@ package com.gongjakso.server.domain.team.repository; import com.gongjakso.server.domain.team.dto.response.SimpleTeamRes; +import com.gongjakso.server.domain.team.entity.Team; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import java.util.Optional; + public interface TeamRepositoryCustom { + Optional findTeamById(Long id); + Page findPaginationWithContest(Long contestId, String province, String district, Pageable pageable); Page findPaginationWithoutContest(String province, String district, String keyword, Pageable pageable); diff --git a/src/main/java/com/gongjakso/server/domain/team/repository/TeamRepositoryImpl.java b/src/main/java/com/gongjakso/server/domain/team/repository/TeamRepositoryImpl.java index 468a0566..0f22d4f8 100644 --- a/src/main/java/com/gongjakso/server/domain/team/repository/TeamRepositoryImpl.java +++ b/src/main/java/com/gongjakso/server/domain/team/repository/TeamRepositoryImpl.java @@ -10,8 +10,9 @@ import org.springframework.data.domain.Pageable; import java.util.List; +import java.util.Optional; -import static com.gongjakso.server.domain.team.entity.QApply.apply; +import static com.gongjakso.server.domain.apply.entity.QApply.apply; import static com.gongjakso.server.domain.team.entity.QScrap.scrap; import static com.gongjakso.server.domain.team.entity.QTeam.team; @@ -20,6 +21,15 @@ public class TeamRepositoryImpl implements TeamRepositoryCustom { private final JPAQueryFactory queryFactory; + public Optional findTeamById(Long id) { + return Optional.ofNullable(queryFactory + .selectFrom(team) + .join(team.member).fetchJoin() + .where(team.id.eq(id) + .and(team.deletedAt.isNull())) + .fetchOne()); + } + public Page findPaginationWithContest(Long contestId, String province, String district, Pageable pageable) { BooleanBuilder builder = new BooleanBuilder(); @@ -31,30 +41,30 @@ public Page findPaginationWithContest(Long contestId, String prov } List teamList = queryFactory - .select(team) - .from(team) - .where( - team.contest.id.eq(contestId), - team.deletedAt.isNull(), - builder - ) - .orderBy(team.createdAt.desc()) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); + .select(team) + .from(team) + .where( + team.contest.id.eq(contestId), + team.deletedAt.isNull(), + builder + ) + .orderBy(team.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); List content = teamList.stream() - .map(SimpleTeamRes::of) - .toList(); + .map(SimpleTeamRes::of) + .toList(); Long total = queryFactory.select(team.count()) - .from(team) - .where( - team.province.eq(province), - team.district.eq(district), - team.deletedAt.isNull() - ) - .fetchOne(); + .from(team) + .where( + team.province.eq(province), + team.district.eq(district), + team.deletedAt.isNull() + ) + .fetchOne(); return new PageImpl<>(content, pageable, (total != null) ? total : 0); } @@ -73,83 +83,83 @@ public Page findPaginationWithoutContest(String province, String } List teamList = queryFactory - .select(team) - .from(team) - .where(team.deletedAt.isNull() - .and(builder)) - .orderBy(team.createdAt.desc()) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); + .select(team) + .from(team) + .where(team.deletedAt.isNull() + .and(builder)) + .orderBy(team.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); List content = teamList.stream() .map(SimpleTeamRes::of) .toList(); Long total = queryFactory.select(team.count()) - .from(team) - .where( - team.province.eq(province), - team.district.eq(district), - team.title.containsIgnoreCase(keyword), - team.deletedAt.isNull() - ) - .fetchOne(); + .from(team) + .where( + team.province.eq(province), + team.district.eq(district), + team.title.containsIgnoreCase(keyword), + team.deletedAt.isNull() + ) + .fetchOne(); return new PageImpl<>(content, pageable, (total != null) ? total : 0); } public Page findRecruitPagination(Long memberId, Pageable pageable) { List teamList = queryFactory - .select(team) - .from(team) - .where( - team.member.id.eq(memberId), - team.deletedAt.isNull() - ) - .orderBy(team.createdAt.desc()) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); + .select(team) + .from(team) + .where( + team.member.id.eq(memberId), + team.deletedAt.isNull() + ) + .orderBy(team.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); List content = teamList.stream() - .map(SimpleTeamRes::of) - .toList(); + .map(SimpleTeamRes::of) + .toList(); Long total = queryFactory.select(team.count()) - .from(team) - .where( - team.member.id.eq(memberId), - team.deletedAt.isNull() - ) - .fetchOne(); + .from(team) + .where( + team.member.id.eq(memberId), + team.deletedAt.isNull() + ) + .fetchOne(); return new PageImpl<>(content, pageable, (total != null) ? total : 0); } public Page findApplyPagination(Long memberId, Pageable pageable) { List teamList = queryFactory - .select(team) - .from(team) - .innerJoin(apply).on(apply.team.id.eq(team.id).and(apply.deletedAt.isNull())) - .where( - team.deletedAt.isNull() - ) - .orderBy(team.createdAt.desc()) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); + .select(team) + .from(team) + .innerJoin(apply).on(apply.team.id.eq(team.id).and(apply.deletedAt.isNull())) + .where( + team.deletedAt.isNull() + ) + .orderBy(team.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); List content = teamList.stream() - .map(SimpleTeamRes::of) - .toList(); + .map(SimpleTeamRes::of) + .toList(); Long total = queryFactory.select(team.count()) - .from(team) - .where( - team.deletedAt.isNull() - ) - .fetchOne(); + .from(team) + .where( + team.deletedAt.isNull() + ) + .fetchOne(); return new PageImpl<>(content, pageable, (total != null) ? total : 0); } @@ -162,27 +172,27 @@ public Page findScrapPagination(Long memberId, Pageable pageable) .fetch(); List teamList = queryFactory - .select(team) - .from(team) - .where( - team.id.in(teamIdList), - team.deletedAt.isNull() - ) - .orderBy(team.createdAt.desc()) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); + .select(team) + .from(team) + .where( + team.id.in(teamIdList), + team.deletedAt.isNull() + ) + .orderBy(team.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); List content = teamList.stream() - .map(SimpleTeamRes::of) - .toList(); + .map(SimpleTeamRes::of) + .toList(); Long total = queryFactory.select(team.count()) - .from(team) - .where( - team.deletedAt.isNull() - ) - .fetchOne(); + .from(team) + .where( + team.deletedAt.isNull() + ) + .fetchOne(); return new PageImpl<>(content, pageable, (total != null) ? total : 0); } diff --git a/src/main/java/com/gongjakso/server/global/config/SecurityConfig.java b/src/main/java/com/gongjakso/server/global/config/SecurityConfig.java index dae3309c..4f98de6e 100644 --- a/src/main/java/com/gongjakso/server/global/config/SecurityConfig.java +++ b/src/main/java/com/gongjakso/server/global/config/SecurityConfig.java @@ -69,6 +69,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/api/v1/auth/**").permitAll() .requestMatchers("/api/v1/member/**").authenticated() .requestMatchers("/api/v1/contest/{contest_id}/team/**").authenticated() + .requestMatchers("/api/v2/apply/**").authenticated() // 이외의 모든 요청은 인증 정보 필요 .anyRequest().permitAll()); diff --git a/src/main/java/com/gongjakso/server/global/exception/ErrorCode.java b/src/main/java/com/gongjakso/server/global/exception/ErrorCode.java index 1d015df7..97a55700 100644 --- a/src/main/java/com/gongjakso/server/global/exception/ErrorCode.java +++ b/src/main/java/com/gongjakso/server/global/exception/ErrorCode.java @@ -39,7 +39,14 @@ public enum ErrorCode { PORTFOLIO_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, 6000, "포트폴리오를 찾을 수 없습니다."), PORTFOLIO_SAVE_FAILED_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, 6001, "포트폴리오 저장에 실패했습니다."), PORTFOLIO_UPDATE_FAILED_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, 6002, "포트폴리오 수정에 실패했습니다."), - INVALID_PORTFOLIO_REQUEST_EXCEPTION(HttpStatus.BAD_REQUEST, 6003, "유효하지 않은 포트폴리오 요청입니다."); + INVALID_PORTFOLIO_REQUEST_EXCEPTION(HttpStatus.BAD_REQUEST, 6003, "유효하지 않은 포트폴리오 요청입니다."), + PORTFOLIO_UNAUTHORIZED_EXCEPTION(HttpStatus.UNAUTHORIZED, 6004, "포트폴리오에 대한 권한이 없습니다."), + + //7000: Apply Error + APPLY_PERIOD_EXPIRED_EXCEPTION(HttpStatus.BAD_REQUEST,7000,"지원 기간이 지났습니다"), + APPLY_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND,7001,"존재하지 않는 지원서입니다."), + APPLY_ALREADY_EXISTS_EXCEPTION(HttpStatus.BAD_REQUEST, 7002, "이미 지원했습니다."), + APPLY_LEADER_NOT_ALLOWED_EXCEPTION(HttpStatus.BAD_REQUEST, 7003, "팀장은 지원할 수 없습니다."); private final HttpStatus httpStatus; private final Integer code;