-
Notifications
You must be signed in to change notification settings - Fork 3
[DDING-game] 로고 짝 맞추기 게임 관련 API 구현 #368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
dd0d60e
85792f4
b20c6d8
2811f64
19fe204
4cbf1f8
5006dc1
817e76e
f2f3c46
797aaed
62fb8b3
05f4df8
ef8b07a
bca624f
6f77f82
51e21ba
c6945aa
3478507
1f9673b
cc3b983
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package ddingdong.ddingdongBE.common.exception; | ||
|
|
||
| import static org.springframework.http.HttpStatus.BAD_REQUEST; | ||
|
|
||
| public class FileException extends CustomException { | ||
|
|
||
| private static final String UPLOADED_FILE_NOT_FOUND_MESSAGE = "업로드 할 파일이 없습니다."; | ||
| private static final String FILE_READING_ERROR_MESSAGE = "파일을 읽을 수 없습니다."; | ||
|
|
||
| public FileException(String message, int errorCode) { super(message, errorCode); } | ||
|
|
||
| public static final class UploadedFileNotFoundException extends FileException { | ||
|
|
||
| public UploadedFileNotFoundException() { | ||
| super(UPLOADED_FILE_NOT_FOUND_MESSAGE, BAD_REQUEST.value()); | ||
| } | ||
| } | ||
|
|
||
| public static final class FileReadingException extends FileException { | ||
|
|
||
| public FileReadingException() { | ||
| super(FILE_READING_ERROR_MESSAGE, BAD_REQUEST.value()); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package ddingdong.ddingdongBE.common.exception; | ||
|
|
||
| import static org.springframework.http.HttpStatus.BAD_REQUEST; | ||
|
|
||
| public class PairGameApplierException extends CustomException { | ||
|
|
||
| private static final String DUPLICATED_PAIR_GAME_APPLIER_MESSAGE = "이미 존재하는 응모자입니다."; | ||
| public PairGameApplierException(String message, int errorCode) { | ||
| super(message, errorCode); | ||
| } | ||
|
|
||
| public static final class DuplicatedPairGameApplierException extends PairGameApplierException { | ||
| public DuplicatedPairGameApplierException() { | ||
| super(DUPLICATED_PAIR_GAME_APPLIER_MESSAGE, BAD_REQUEST.value()); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package ddingdong.ddingdongBE.domain.pairgame.api; | ||
|
|
||
| import ddingdong.ddingdongBE.domain.pairgame.controller.dto.request.CreatePairGameApplierRequest; | ||
| import ddingdong.ddingdongBE.domain.pairgame.controller.dto.response.PairGameApplierAmountResponse; | ||
| import ddingdong.ddingdongBE.domain.pairgame.controller.dto.response.PairGameMetaDataResponse; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import jakarta.validation.Valid; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.web.bind.annotation.*; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| @Tag(name = "PairGame - User", description = "User PairGame API") | ||
| @RequestMapping("/server") | ||
| public interface UserPairGameApi { | ||
|
|
||
| @Operation(summary = "응모자 생성 API") | ||
| @ApiResponse(responseCode = "201", description = "응모자 생성 성공") | ||
| @ResponseStatus(HttpStatus.CREATED) | ||
| @PostMapping("/pair-game/appliers") | ||
| void createPairGameApplier( | ||
| @Valid @RequestPart("request") CreatePairGameApplierRequest request, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p1) RequestPart 필요한가요? 내부에 MultipartFile이 존재하다면, 사용한다는데 분리되어있는 것 같아서요!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JSON 데이터와 MultipartFile를 함께 받으려면 RequestPart로 각각 받아야 한다고 알고 있어서 해당 방법으로 구현했습니다! 찾아보니 @ModelAttribute로 한 번에 받는 방법도 있는데, 혹시 해당 방법으로 바꿀 필요가 있을까요?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. requestdto 내부에 Multipartfile이 존재하지 않고, 따로 파라미터로 받고 있어서 질문하였습니다.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @RequestPart로 MultipartFile을 받으려면 dto 내부에 위치하면 안 되는 것으로 알고 있어 해당 방식으로 구현하였습니다..!! 혹시 dto 내부에 위치시키는 다른 방식이 있을까요? 해당 방식은 프론트에서 form-data 형식으로 각각 key 지정해서 보내주면 되는 것 같습니다! |
||
| @RequestPart("file") MultipartFile file | ||
| ); | ||
|
|
||
| @Operation(summary = "응모자 현황 조회 API") | ||
| @ApiResponse(responseCode = "200", description = "응모자 현황 조회 성공") | ||
| @ResponseStatus(HttpStatus.OK) | ||
| @GetMapping("/pair-game/appliers/amount") | ||
| PairGameApplierAmountResponse getPairGameApplierAmount(); | ||
|
|
||
| @Operation(summary = "게임 메타데이터 조회 API") | ||
| @ApiResponse(responseCode = "200", description = "게임 메타데이터 조회 성공") | ||
| @ResponseStatus(HttpStatus.OK) | ||
| @GetMapping("/pair-game/metadata") | ||
| PairGameMetaDataResponse getPairGameMetaData(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package ddingdong.ddingdongBE.domain.pairgame.controller; | ||
|
|
||
| import ddingdong.ddingdongBE.domain.pairgame.api.UserPairGameApi; | ||
| import ddingdong.ddingdongBE.domain.pairgame.controller.dto.request.CreatePairGameApplierRequest; | ||
| import ddingdong.ddingdongBE.domain.pairgame.controller.dto.response.PairGameApplierAmountResponse; | ||
| import ddingdong.ddingdongBE.domain.pairgame.controller.dto.response.PairGameMetaDataResponse; | ||
| import ddingdong.ddingdongBE.domain.pairgame.service.FacadeUserPairGameService; | ||
| import ddingdong.ddingdongBE.domain.pairgame.service.dto.query.PairGameApplierAmountQuery; | ||
| import ddingdong.ddingdongBE.domain.pairgame.service.dto.query.PairGameMetaDataQuery; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| public class UserPairGameController implements UserPairGameApi { | ||
|
|
||
| private final FacadeUserPairGameService facadeUserPairGameService; | ||
|
|
||
| @Override | ||
| public void createPairGameApplier(CreatePairGameApplierRequest createPairGameApplierRequest, MultipartFile studentFeeImageFile) { | ||
| facadeUserPairGameService.createPairGameApplier(createPairGameApplierRequest.toCommand(studentFeeImageFile)); | ||
| } | ||
|
|
||
| @Override | ||
| public PairGameApplierAmountResponse getPairGameApplierAmount() { | ||
| PairGameApplierAmountQuery query = facadeUserPairGameService.getPairGameApplierAmount(); | ||
| return PairGameApplierAmountResponse.from(query); | ||
| } | ||
|
|
||
| @Override | ||
| public PairGameMetaDataResponse getPairGameMetaData() { | ||
| PairGameMetaDataQuery query = facadeUserPairGameService.getPairGameMetaData(); | ||
| return PairGameMetaDataResponse.from(query); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package ddingdong.ddingdongBE.domain.pairgame.controller.dto.request; | ||
|
|
||
| import ddingdong.ddingdongBE.domain.pairgame.service.dto.command.CreatePairGameApplierCommand; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| public record CreatePairGameApplierRequest ( | ||
|
|
||
| @NotNull(message = "응모자 이름은 필수 입력 사항입니다.") | ||
| @Schema(description = "응모자 이름", example = "김띵동") | ||
| String name, | ||
|
|
||
| @NotNull(message = "응모자 학과는 필수 입력 사항입니다.") | ||
| @Schema(description = "응모자 학과", example = "융합소프트웨어학부") | ||
| String department, | ||
|
|
||
| @NotNull(message = "응모자 학번은 필수 입력 사항입니다.") | ||
| @Schema(description = "응모자 학번", example = "60000000") | ||
| String studentNumber, | ||
|
|
||
| @NotNull(message = "응모자 전화번호는 필수 입력 사항입니다.") | ||
| @Schema(description = "응모자 전화번호", example = "010-0000-0000") | ||
| String phoneNumber | ||
| ) { | ||
| public CreatePairGameApplierCommand toCommand(MultipartFile studentFeeImageFile) { | ||
| return CreatePairGameApplierCommand.builder() | ||
| .name(name) | ||
| .department(department) | ||
| .studentNumber(studentNumber) | ||
| .phoneNumber(phoneNumber) | ||
| .studentFeeImageFile(studentFeeImageFile) | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package ddingdong.ddingdongBE.domain.pairgame.controller.dto.response; | ||
|
|
||
| import ddingdong.ddingdongBE.domain.pairgame.service.dto.query.PairGameApplierAmountQuery; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import lombok.Builder; | ||
|
|
||
| @Builder | ||
| public record PairGameApplierAmountResponse( | ||
| @Schema(description = "총 응모자 수", example = "200") | ||
| int amount | ||
| ) { | ||
| public static PairGameApplierAmountResponse from(PairGameApplierAmountQuery query) { | ||
| return PairGameApplierAmountResponse.builder() | ||
| .amount(query.amount()) | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package ddingdong.ddingdongBE.domain.pairgame.controller.dto.response; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ddingdong.ddingdongBE.domain.pairgame.service.dto.query.PairGameMetaDataQuery.PairGameClubAndImageQuery; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ddingdong.ddingdongBE.domain.pairgame.service.dto.query.PairGameMetaDataQuery; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import io.swagger.v3.oas.annotations.media.ArraySchema; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.Builder; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Builder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public record PairGameMetaDataResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @ArraySchema(schema = @Schema(implementation = PairGameMetaDataResponse.PairGameClubAndImageResponse.class)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<PairGameClubAndImageResponse> metaData | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Builder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public record PairGameClubAndImageResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Schema(description = "동아리 이름", example = "COW") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String clubName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Schema(description = "동아리 분과", example = "사회연구") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String category, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Schema(description = "동아리 로고 이미지 CDN URL", example = "https://cdn.com") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String imageUrl | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public static PairGameClubAndImageResponse from(PairGameClubAndImageQuery query) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return PairGameClubAndImageResponse.builder() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .clubName(query.clubName()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .category(query.category()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .imageUrl(query.imageUrl()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public static PairGameMetaDataResponse from(PairGameMetaDataQuery query) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<PairGameClubAndImageResponse> responses = query.metaData().stream() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(PairGameClubAndImageResponse::from) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .toList(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+25
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🛠️ 수정 제안 import lombok.Builder;
import java.util.List;
+import java.util.Objects;
) {
public static PairGameClubAndImageResponse from(PairGameClubAndImageQuery query) {
+ Objects.requireNonNull(query, "query must not be null");
return PairGameClubAndImageResponse.builder()
.clubName(query.clubName())
.category(query.category())
.imageUrl(query.imageUrl())
.build();
}
}
public static PairGameMetaDataResponse from(PairGameMetaDataQuery query) {
- List<PairGameClubAndImageResponse> responses = query.metaData().stream()
- .map(PairGameClubAndImageResponse::from)
- .toList();
+ Objects.requireNonNull(query, "query must not be null");
+ List<PairGameClubAndImageResponse> responses =
+ query.metaData() == null ? List.of()
+ : query.metaData().stream()
+ .map(PairGameClubAndImageResponse::from)
+ .toList();
return PairGameMetaDataResponse.builder()
.metaData(responses)
.build();
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return PairGameMetaDataResponse.builder() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .metaData(responses) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package ddingdong.ddingdongBE.domain.pairgame.entity; | ||
|
|
||
| import ddingdong.ddingdongBE.common.BaseEntity; | ||
| import jakarta.persistence.*; | ||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import org.hibernate.annotations.SQLDelete; | ||
| import org.hibernate.annotations.SQLRestriction; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Entity | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Getter | ||
| @SQLDelete(sql = "update pair_game_applier set deleted_at = CURRENT_TIMESTAMP where id=?") | ||
| @SQLRestriction("deleted_at IS NULL") | ||
Seooooo24 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| public class PairGameApplier extends BaseEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(nullable = false, length = 50) | ||
| private String name; | ||
|
|
||
| @Column(nullable = false, length = 50) | ||
| private String department; | ||
|
|
||
| @Column(unique = true, nullable = false, length = 50) | ||
| private String studentNumber; | ||
|
|
||
| @Column(nullable = false, length = 50) | ||
| private String phoneNumber; | ||
|
|
||
| @Column(nullable = false) | ||
| private String studentFeeImageUrl; | ||
|
|
||
| @Column(columnDefinition = "TIMESTAMP") | ||
| private LocalDateTime deletedAt; | ||
|
|
||
| @Builder | ||
| private PairGameApplier(String name, String department, String studentNumber, String phoneNumber, String studentFeeImageUrl) { | ||
| this.name = name; | ||
| this.department = department; | ||
| this.studentNumber = studentNumber; | ||
| this.phoneNumber = phoneNumber; | ||
| this.studentFeeImageUrl = studentFeeImageUrl; | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package ddingdong.ddingdongBE.domain.pairgame.repository; | ||
|
|
||
| import ddingdong.ddingdongBE.domain.pairgame.entity.PairGameApplier; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
|
|
||
| public interface PairGameRepository extends JpaRepository<PairGameApplier, Long> { | ||
| boolean existsByStudentNumber(String studentNumber); | ||
| } | ||
Seooooo24 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n -A 5 'getAllByIds' --type javaRepository: COW-dev/ddingdong-be
Length of output: 4933
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 1255
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 6617
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 3216
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 762
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 591
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 46
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 1953
getAllByIds메서드의 반환 데이터 검증 필요findAllById는 존재하지 않는 ID에 대해 예외를 발생시키지 않고 찾은 엔티티만 반환합니다. FileMetaData는 Club과의 외래 키 제약이 없어entityId가 삭제된 Club을 참조할 수 있으며, 이 경우clubNameMap.get(file.getEntityId())가 null을 반환합니다.특히
FacadeUserPairGameService(line 60)에서 null 값이 response에 포함되므로, 반환된 데이터 크기 검증 또는 null 체크 로직이 필요합니다.🤖 Prompt for AI Agents