diff --git a/build.gradle b/build.gradle index 198831cd..6b22d9da 100644 --- a/build.gradle +++ b/build.gradle @@ -78,6 +78,9 @@ dependencies { annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + // Json Type Parsing + implementation 'com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations' } 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 new file mode 100644 index 00000000..20c4ba1b --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/portfolio/controller/PortfolioController.java @@ -0,0 +1,58 @@ +package com.gongjakso.server.domain.portfolio.controller; + +import com.gongjakso.server.domain.portfolio.dto.request.PortfolioReq; +import com.gongjakso.server.domain.portfolio.dto.response.PortfolioRes; +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 jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("api/v2/mypage/portfolio") +public class PortfolioController { + + private final PortfolioService portfolioService; + + @Operation(description = "포트폴리오 등록 API") + @PostMapping("") + public ApplicationResponse registerPortfolio(@AuthenticationPrincipal PrincipalDetails principalDetails, + @Valid @RequestBody PortfolioReq portfolioReq) { + return ApplicationResponse.ok(portfolioService.registerPortfolio(principalDetails.getMember(), portfolioReq)); + } + + @Operation(description = "포트폴리오 상세 조회 API") + @GetMapping("/{portfolio_id}") + public ApplicationResponse getPortfolio(@AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable("portfolio_id") Long portfolioId) { + return ApplicationResponse.ok(portfolioService.getPortfolio(principalDetails.getMember(), portfolioId)); + } + + @Operation(description = "포트폴리오 수정 API") + @PutMapping("/{portfolio_id}") + public ApplicationResponse updatePortfolio(@AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable("portfolio_id") Long portfolioId, + @Valid @RequestBody PortfolioReq portfolioReq) { + return ApplicationResponse.ok(portfolioService.updatePortfolio(principalDetails.getMember(), portfolioId, portfolioReq)); + } + + @Operation(description = "포트폴리오 삭제 API") + @DeleteMapping("/{portfolio_id}") + public ApplicationResponse deletePortfolio(@AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable("portfolio_id") Long portfolioId) { + portfolioService.deletePortfolio(principalDetails.getMember(), portfolioId); + + return ApplicationResponse.ok(); + } +} diff --git a/src/main/java/com/gongjakso/server/domain/portfolio/dto/request/PortfolioReq.java b/src/main/java/com/gongjakso/server/domain/portfolio/dto/request/PortfolioReq.java new file mode 100644 index 00000000..0248b7e4 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/portfolio/dto/request/PortfolioReq.java @@ -0,0 +1,54 @@ +package com.gongjakso.server.domain.portfolio.dto.request; + +import java.time.LocalDate; +import java.util.List; +import lombok.Builder; + +@Builder +public record PortfolioReq ( + List educationList, + List workList, + List activityList, + List awardList, + List certificateList, + List snsList +) { + public record Education ( + String school, + String grade, + Boolean isActive + ) { + } + + public record Work ( + String company, + String partition, + LocalDate enteredAt, + LocalDate exitedAt, + Boolean isActive, + String detail + ) { + } + + public record Activty ( + String name, + Boolean isActive + ) { + } + public record Award ( + String contestName, + String awardName, + LocalDate awardDate + ) { + } + public record Certificate ( + String name, + String rating, + LocalDate certificationDate + ) { + } + public record Sns ( + String snsLink + ) { + } +} diff --git a/src/main/java/com/gongjakso/server/domain/portfolio/dto/response/PortfolioRes.java b/src/main/java/com/gongjakso/server/domain/portfolio/dto/response/PortfolioRes.java new file mode 100644 index 00000000..8771a902 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/portfolio/dto/response/PortfolioRes.java @@ -0,0 +1,32 @@ +package com.gongjakso.server.domain.portfolio.dto.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.gongjakso.server.domain.portfolio.entity.Portfolio; +import com.gongjakso.server.domain.portfolio.vo.PortfolioData; +import lombok.Builder; +import java.util.List; + +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public record PortfolioRes ( + Long id, + List educationList, + List workList, + List activityList, + List awardList, + List certificateList, + List snsList +) { + public static PortfolioRes from(Portfolio portfolio) { + PortfolioData portfolioData = portfolio.getPortfolioData(); + return new PortfolioRes( + portfolio.getId(), + portfolioData.educationList(), + portfolioData.workList(), + portfolioData.activityList(), + portfolioData.awardList(), + portfolioData.certificateList(), + portfolioData.snsList() + ); + } +} \ No newline at end of file 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 29097334..9de2a751 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 @@ -1,21 +1,45 @@ package com.gongjakso.server.domain.portfolio.entity; +import com.gongjakso.server.domain.member.entity.Member; +import com.gongjakso.server.domain.portfolio.vo.PortfolioData; import com.gongjakso.server.global.common.BaseTimeEntity; import jakarta.persistence.*; +import java.time.LocalDate; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.annotations.SQLDelete; +import org.hibernate.type.SqlTypes; @Getter @Entity @Table(name = "portfolio") -@SQLDelete(sql = "UPDATE portfolio SET deleted_at = NOW() where id = ?") +@SQLDelete(sql = "UPDATE portfolio SET deleted_at = NOW() where portfolio_id = ?") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Portfolio extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id",nullable = false,columnDefinition = "bigint") + @Column(name = "portfolio_id", nullable = false, columnDefinition = "bigint") private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Column(name = "portfolio_data", columnDefinition = "json") + @JdbcTypeCode(SqlTypes.JSON) + private PortfolioData portfolioData; + + @Builder + public Portfolio(Member member, PortfolioData portfolioData) { + this.member = member; + this.portfolioData = portfolioData; + } + + public void update(PortfolioData updatedData) { + this.portfolioData = updatedData; + } } 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 new file mode 100644 index 00000000..d61eea7f --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/portfolio/repository/PortfolioRepository.java @@ -0,0 +1,9 @@ +package com.gongjakso.server.domain.portfolio.repository; + +import com.gongjakso.server.domain.portfolio.entity.Portfolio; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PortfolioRepository extends JpaRepository { + Optional findByIdAndDeletedAtIsNull(Long portfolioId); +} 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 new file mode 100644 index 00000000..6c8c2f3f --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/portfolio/service/PortfolioService.java @@ -0,0 +1,139 @@ +package com.gongjakso.server.domain.portfolio.service; + +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.entity.Portfolio; +import com.gongjakso.server.domain.portfolio.vo.PortfolioData; +import com.gongjakso.server.domain.portfolio.repository.PortfolioRepository; +import com.gongjakso.server.global.exception.ApplicationException; +import com.gongjakso.server.global.exception.ErrorCode; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class PortfolioService { + + private final PortfolioRepository portfolioRepository; + + // PortfolioReq -> PortfolioData 변환 + private PortfolioData convertToPortfolioData(PortfolioReq portfolioReq) { + List educationList = portfolioReq.educationList() != null + ? portfolioReq.educationList().stream() + .map(education -> new PortfolioData.Education( + education.school(), + education.grade(), + education.isActive() + )) + .toList() + : List.of(); + + List workList = portfolioReq.workList() != null + ? portfolioReq.workList().stream() + .map(work -> new PortfolioData.Work( + work.company(), + work.partition(), + work.enteredAt(), + work.exitedAt(), + work.isActive(), + work.detail() + )) + .toList() + : List.of(); + + List activityList = portfolioReq.activityList() != null + ? portfolioReq.activityList().stream() + .map(activity -> new PortfolioData.Activity( + activity.name(), + activity.isActive() + )) + .toList() + : List.of(); + + List awardList = portfolioReq.awardList() != null + ? portfolioReq.awardList().stream() + .map(award -> new PortfolioData.Award( + award.contestName(), + award.awardName(), + award.awardDate() + )) + .toList() + : List.of(); + + List certificateList = portfolioReq.certificateList() != null + ? portfolioReq.certificateList().stream() + .map(certificate -> new PortfolioData.Certificate( + certificate.name(), + certificate.rating(), + certificate.certificationDate() + )) + .toList() + : List.of(); + + List snsList = portfolioReq.snsList() != null + ? portfolioReq.snsList().stream() + .map(sns -> new PortfolioData.Sns( + sns.snsLink() + )) + .toList() + : List.of(); + + return new PortfolioData(educationList, workList, activityList, awardList, certificateList, snsList); + } + + @Transactional + public PortfolioRes registerPortfolio(Member member, PortfolioReq portfolioReq) { + if (member == null) { + throw new ApplicationException(ErrorCode.UNAUTHORIZED_EXCEPTION); + } + PortfolioData portfolioData = convertToPortfolioData(portfolioReq); + Portfolio portfolio = Portfolio.builder() + .member(member) + .portfolioData(portfolioData) + .build(); + Portfolio savedPortfolio = portfolioRepository.save(portfolio); + + return PortfolioRes.from(savedPortfolio); + } + + public PortfolioRes getPortfolio(Member member, Long portfolioId) { + Portfolio portfolio = portfolioRepository.findByIdAndDeletedAtIsNull(portfolioId) + .orElseThrow(() -> new ApplicationException(ErrorCode.PORTFOLIO_NOT_FOUND_EXCEPTION)); + if (!portfolio.getMember().getId().equals(member.getId())) { + throw new ApplicationException(ErrorCode.FORBIDDEN_EXCEPTION); + } + + return PortfolioRes.from(portfolio); + } + + @Transactional + public PortfolioRes updatePortfolio(Member member, Long portfolioId, PortfolioReq portfolioReq) { + Portfolio portfolio = portfolioRepository.findById(portfolioId) + .orElseThrow(() -> new ApplicationException(ErrorCode.PORTFOLIO_NOT_FOUND_EXCEPTION)); + if (!portfolio.getMember().getId().equals(member.getId())) { + throw new ApplicationException(ErrorCode.FORBIDDEN_EXCEPTION); + } + PortfolioData updatedPortfolioData = convertToPortfolioData(portfolioReq); + portfolio.update(updatedPortfolioData); + Portfolio updatedPortfolio = portfolioRepository.save(portfolio); + + return PortfolioRes.from(updatedPortfolio); + } + + @Transactional + public void deletePortfolio(Member member, Long portfolioId) { + Portfolio portfolio = portfolioRepository.findById(portfolioId) + .orElseThrow(() -> new ApplicationException(ErrorCode.PORTFOLIO_NOT_FOUND_EXCEPTION)); + if (portfolio.getDeletedAt() != null) { + throw new ApplicationException(ErrorCode.ALREADY_DELETE_EXCEPTION); + } + if (!portfolio.getMember().getId().equals(member.getId())) { + throw new ApplicationException(ErrorCode.FORBIDDEN_EXCEPTION); + } + portfolioRepository.delete(portfolio); + } +} \ No newline at end of file diff --git a/src/main/java/com/gongjakso/server/domain/portfolio/vo/PortfolioData.java b/src/main/java/com/gongjakso/server/domain/portfolio/vo/PortfolioData.java new file mode 100644 index 00000000..28f80f00 --- /dev/null +++ b/src/main/java/com/gongjakso/server/domain/portfolio/vo/PortfolioData.java @@ -0,0 +1,50 @@ +package com.gongjakso.server.domain.portfolio.vo; + +import java.time.LocalDate; +import java.util.List; + +public record PortfolioData ( + List educationList, + List workList, + List activityList, + List awardList, + List certificateList, + List snsList +) { + public record Education ( + String school, + String grade, + Boolean isActive + ) { + } + public record Work ( + String company, + String partition, + LocalDate enteredAt, + LocalDate exitedAt, + Boolean isActive, + String detail + ) { + } + public record Activity ( + String name, + Boolean isActive + ) { + } + public record Award ( + String contestName, + String awardName, + LocalDate awardDate + ) { + } + public record Certificate ( + String name, + String rating, + LocalDate certificationDate + ) { + } + public record Sns ( + String snsLink + ) { + } +} 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 d4fcb59a..1d015df7 100644 --- a/src/main/java/com/gongjakso/server/global/exception/ErrorCode.java +++ b/src/main/java/com/gongjakso/server/global/exception/ErrorCode.java @@ -33,9 +33,13 @@ public enum ErrorCode { // 5000: Team Error TEAM_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, 5000, "존재하지 않는 팀입니다."), - INVALID_POSITION_EXCEPTION(HttpStatus.BAD_REQUEST, 5001, "올바르지 않은 포지션입니다."); + INVALID_POSITION_EXCEPTION(HttpStatus.BAD_REQUEST, 5001, "올바르지 않은 포지션입니다."), // 6000: Portfolio Error + 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, "유효하지 않은 포트폴리오 요청입니다."); private final HttpStatus httpStatus; private final Integer code;