Repository navigation
[Feat/#139] 열린피드백 관리자 API 추가 #140
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
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
446d15b
fix: 열린피드백 회차 판정과 질문 시각에 한국 시간대를 명시
xeoxxn 14fb958
feat: 열린피드백 회차에 답변 공개 시각을 추가
xeoxxn 2ab16f0
feat: 열린피드백 회차 상태와 답변 집계를 도메인에 추가
xeoxxn 897cbf2
feat: 관리자 회차 조회와 질문 검색에 필요한 저장소 메서드를 추가
xeoxxn 486cf8e
feat: 열린피드백 회차 관리와 답변 임시저장·공개 서비스를 추가
xeoxxn 0a23b80
feat: 열린피드백 관리자 API를 추가
xeoxxn 08674ea
Merge remote-tracking branch 'origin/main' into feat/#139-open-feedba…
xeoxxn 8127a1f
fix: 열린피드백 질문 검색어의 LIKE 와일드카드를 이스케이프
xeoxxn 018eda6
refactor: 열린피드백 질문·회차 도메인을 record에서 클래스로 변경
xeoxxn 80de2a1
refactor: 열린피드백 운영진 기능을 AdminFeedbackService로 분리
xeoxxn 197567a
refactor: 열린피드백 운영진 API 경로를 /v1/admin/feedback 아래로 이동
xeoxxn 431fb13
fix: 답변 공개를 질문 접수가 끝난 회차에서만 허용
xeoxxn 3173d76
fix: 진행 중 회차를 회차 번호 대신 접수 시작일 기준으로 선택
xeoxxn 058cf52
refactor: 회차 기간·겹침 검증을 validate 메서드로 분리
xeoxxn 0236e31
refactor: 회차 집계 결과를 FeedbackCountProjection record로 조회
xeoxxn 9902e5d
fix: 답변 저장 시 바뀐 컬럼만 UPDATE하도록 @DynamicUpdate 추가
xeoxxn 7fb246e
fix: 회차 번호 동시 배정의 유니크 위반을 409로 응답
xeoxxn b48629f
refactor: 열린피드백 질문 작성자 이름을 member로 통일
xeoxxn fe9f24b
feat: 답변 저장 요청에 같은 질문이 중복되면 거절
xeoxxn 0e34f9e
chore: Flyway 마이그레이션 버전 조정
xeoxxn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
35 changes: 35 additions & 0 deletions
35
...n-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/feedback/AdminFeedbackApi.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package kr.ac.kookmin.stream.api.admin.welfare.feedback; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import kr.ac.kookmin.stream.api.admin.AdminApiUser; | ||
| import kr.ac.kookmin.stream.api.admin.welfare.feedback.response.AdminFeedbackResponse; | ||
| import kr.ac.kookmin.stream.api.common.dto.ApiResponse; | ||
| import kr.ac.kookmin.stream.api.common.dto.PageParams; | ||
| import kr.ac.kookmin.stream.api.common.dto.PageResponse; | ||
| import kr.ac.kookmin.stream.api.common.openapi.ApiErrorCode; | ||
| import kr.ac.kookmin.stream.common.CommonErrorCode; | ||
| import kr.ac.kookmin.stream.welfare.domain.feedback.domain.FeedbackErrorCode; | ||
| import org.springdoc.core.annotations.ParameterObject; | ||
|
|
||
| /** | ||
| * 운영진 열린피드백 질문 API의 문서 명세. 구현은 {@link AdminFeedbackController}가 맡는다. | ||
| * <p> | ||
| * 스웨거 문서용 어노테이션만 이쪽에 두고 컨트롤러에는 라우팅과 본문만 남긴다. | ||
| */ | ||
| @Tag(name = "열린피드백 질문 관리", description = "운영진 열린피드백 질문 조회") | ||
| public interface AdminFeedbackApi { | ||
|
|
||
| /** 현재 회차 질문 탭. 질문 내용·작성자 이름으로 검색한다. */ | ||
| @Operation(summary = "사용자 질문 조회", | ||
| description = "회차(roundId)의 질문을 질문 시각 내림차순으로 조회한다. roundId를 생략하면 진행 중 회차를 보고, " | ||
| + "진행 중 회차도 없으면 빈 목록이다. keyword는 질문 내용과 작성자 이름에 부분 일치로 걸린다.") | ||
| @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) | ||
| @ApiErrorCode(type = FeedbackErrorCode.class, codes = {"FEEDBACK_ROUND_NOT_FOUND"}) | ||
| ApiResponse<PageResponse<AdminFeedbackResponse>> getFeedbacks( | ||
| AdminApiUser apiUser, | ||
| Long roundId, | ||
| String keyword, | ||
| @ParameterObject PageParams pageParams | ||
| ); | ||
| } |
39 changes: 39 additions & 0 deletions
39
...rc/main/java/kr/ac/kookmin/stream/api/admin/welfare/feedback/AdminFeedbackController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package kr.ac.kookmin.stream.api.admin.welfare.feedback; | ||
|
|
||
| import jakarta.validation.Valid; | ||
| import kr.ac.kookmin.stream.api.admin.AdminApiUser; | ||
| import kr.ac.kookmin.stream.api.admin.welfare.feedback.response.AdminFeedbackResponse; | ||
| import kr.ac.kookmin.stream.api.admin.welfare.feedback.usecase.AdminFeedbackSearchUseCase; | ||
| import kr.ac.kookmin.stream.api.admin.welfare.feedback.usecase.MemberFeedback; | ||
| import kr.ac.kookmin.stream.api.common.dto.ApiResponse; | ||
| import kr.ac.kookmin.stream.api.common.dto.PageParams; | ||
| import kr.ac.kookmin.stream.api.common.dto.PageResponse; | ||
| import kr.ac.kookmin.stream.common.PageResult; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.ModelAttribute; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/v1/admin/feedback/questions") | ||
| @RequiredArgsConstructor | ||
| public class AdminFeedbackController implements AdminFeedbackApi { | ||
|
|
||
| private final AdminFeedbackSearchUseCase adminFeedbackSearchUseCase; | ||
|
|
||
| @Override | ||
| @GetMapping | ||
| public ApiResponse<PageResponse<AdminFeedbackResponse>> getFeedbacks( | ||
| AdminApiUser apiUser, | ||
| @RequestParam(required = false) Long roundId, | ||
| @RequestParam(required = false) String keyword, | ||
| @Valid @ModelAttribute PageParams pageParams | ||
| ) { | ||
| PageResult<MemberFeedback> result = | ||
| adminFeedbackSearchUseCase.search(roundId, keyword, pageParams.toOffset()); | ||
|
|
||
| return ApiResponse.success(PageResponse.from(result, AdminFeedbackResponse::from)); | ||
| } | ||
| } |
88 changes: 88 additions & 0 deletions
88
.../src/main/java/kr/ac/kookmin/stream/api/admin/welfare/feedback/AdminFeedbackRoundApi.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| package kr.ac.kookmin.stream.api.admin.welfare.feedback; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import kr.ac.kookmin.stream.api.admin.AdminApiUser; | ||
| import kr.ac.kookmin.stream.api.admin.welfare.feedback.request.FeedbackAnswerSaveRequest; | ||
| import kr.ac.kookmin.stream.api.admin.welfare.feedback.request.FeedbackRoundCreateRequest; | ||
| import kr.ac.kookmin.stream.api.admin.welfare.feedback.response.FeedbackRoundDetailResponse; | ||
| import kr.ac.kookmin.stream.api.admin.welfare.feedback.response.FeedbackRoundResponse; | ||
| import kr.ac.kookmin.stream.api.common.dto.ApiResponse; | ||
| import kr.ac.kookmin.stream.api.common.dto.PageParams; | ||
| import kr.ac.kookmin.stream.api.common.dto.PageResponse; | ||
| import kr.ac.kookmin.stream.api.common.openapi.ApiErrorCode; | ||
| import kr.ac.kookmin.stream.common.CommonErrorCode; | ||
| import kr.ac.kookmin.stream.welfare.domain.feedback.domain.FeedbackErrorCode; | ||
| import org.springdoc.core.annotations.ParameterObject; | ||
|
|
||
| /** | ||
| * 운영진 열린피드백 회차 API의 문서 명세. 구현은 {@link AdminFeedbackRoundController}가 맡는다. | ||
| * <p> | ||
| * 스웨거 문서용 어노테이션만 이쪽에 두고 컨트롤러에는 라우팅과 본문만 남긴다. 경로 매핑과 | ||
| * 파라미터 바인딩(@{@code RequestParam}, @{@code PathVariable}, @{@code RequestBody} 등)은 구현체에 둔다. | ||
| */ | ||
| @Tag(name = "열린피드백 회차 관리", description = "운영진 열린피드백 회차 등록·조회와 답변 작성·공개") | ||
| public interface AdminFeedbackRoundApi { | ||
|
|
||
| /** 관리 화면 상단 요약. 답변을 아직 공개하지 않은 회차 중 지금 진행 중인 회차를 내려준다. */ | ||
| @Operation(summary = "진행 중 회차 조회", | ||
| description = "답변을 아직 공개하지 않은 회차 중 접수가 시작된 회차(질문접수중·답변작성중)를 시작일이 가장 늦은 것으로 " | ||
| + "조회하고, 그런 회차가 없으면 시작일이 가장 가까운 오픈예정 회차를 조회한다. " | ||
| + "둘 다 없으면 data가 null이다.") | ||
| ApiResponse<FeedbackRoundResponse> getCurrentRound(AdminApiUser apiUser); | ||
|
|
||
| /** 회차 목록. 연도·상태로 필터링하고 페이지 단위로 조회한다. */ | ||
| @Operation(summary = "피드백 회차 목록 조회", | ||
| description = "연도(year)와 상태(status)로 필터링해 회차를 최신순(연도 내림차순 → 회차 내림차순)으로 조회한다. " | ||
| + "상태는 저장하지 않고 접수 기간과 답변 공개 여부로 서버가 판정한다.") | ||
| @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) | ||
| @ApiErrorCode(type = FeedbackErrorCode.class, codes = {"INVALID_FEEDBACK_ROUND_STATUS"}) | ||
| ApiResponse<PageResponse<FeedbackRoundResponse>> getRounds( | ||
| AdminApiUser apiUser, | ||
| Integer year, | ||
| String status, | ||
| @ParameterObject PageParams pageParams | ||
| ); | ||
|
|
||
| /** 회차 등록. 접수 기간만 받고 연도·회차 번호는 서버가 배정한다. */ | ||
| @Operation(summary = "피드백 회차 등록", | ||
| description = "질문 접수 기간(날짜)만 받아 회차를 연다. 시각은 시작일 00:00:00 ~ 마감일 23:59:59로 채우고, " | ||
| + "연도는 시작일 기준, 회차 번호는 그 연도의 최대 + 1로 배정한다. 다른 회차와 접수 기간이 겹치면 409다. " | ||
| + "동시에 등록해 같은 회차 번호가 배정되면 늦은 쪽이 409이고, 다시 요청하면 다음 번호로 등록된다.") | ||
| @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) | ||
| @ApiErrorCode( | ||
| type = FeedbackErrorCode.class, | ||
| codes = {"INVALID_FEEDBACK_ROUND_PERIOD", "FEEDBACK_ROUND_OVERLAPPED", "FEEDBACK_ROUND_ALREADY_EXISTS"} | ||
| ) | ||
| ApiResponse<FeedbackRoundResponse> createRound(AdminApiUser apiUser, FeedbackRoundCreateRequest request); | ||
|
|
||
| /** 회차 답변 보기 모달. 그 회차의 질문 전체를 답변과 함께 내려준다. */ | ||
| @Operation(summary = "회차 답변 내역 조회", | ||
| description = "그 회차에 접수된 질문 전체를 질문 시각 오름차순으로 조회한다. 답변이 없는 질문도 포함된다.") | ||
| @ApiErrorCode(type = FeedbackErrorCode.class, codes = {"FEEDBACK_ROUND_NOT_FOUND"}) | ||
| ApiResponse<FeedbackRoundDetailResponse> getRoundAnswers(AdminApiUser apiUser, Long roundId); | ||
|
|
||
| /** 답변 임시저장. 저장만 하고 학생에게는 보이지 않는다. */ | ||
| @Operation(summary = "피드백 답변 일괄 등록·수정", | ||
| description = "회차 단위로 답변을 저장한다. 보낸 것만 반영하고 이미 답변이 있는 질문은 덮어쓴다. " | ||
| + "한 요청에 같은 질문(feedbackId)을 두 번 보내면 400(INVALID_INPUT)이다. " | ||
| + "여기서 저장한 답변은 관리자에게만 보이고, 답변 공개 API를 불러야 학생 앱에 나타난다.") | ||
| @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) | ||
| @ApiErrorCode( | ||
| type = FeedbackErrorCode.class, | ||
| codes = {"FEEDBACK_NOT_IN_ROUND", "FEEDBACK_ROUND_NOT_FOUND", "FEEDBACK_NOT_FOUND"} | ||
| ) | ||
| ApiResponse<FeedbackRoundDetailResponse> saveAnswers( | ||
| AdminApiUser apiUser, Long roundId, FeedbackAnswerSaveRequest request); | ||
|
|
||
| /** 답변 공개. 그 회차의 답변을 학생 앱에 한꺼번에 띄운다. */ | ||
| @Operation(summary = "피드백 답변 공개", | ||
| description = "그 회차에 작성된 답변을 학생에게 공개한다. 공개는 회차 단위이고, 질문 접수가 끝난 회차(답변작성중)만 " | ||
| + "공개할 수 있다(오픈예정·질문접수중이면 409). " | ||
| + "이미 공개한 회차를 다시 불러도 공개 시각을 바꾸지 않고 성공으로 응답한다(멱등).") | ||
| @ApiErrorCode( | ||
| type = FeedbackErrorCode.class, | ||
| codes = {"FEEDBACK_ANSWER_NOT_FOUND", "FEEDBACK_ROUND_NOT_FOUND", "FEEDBACK_QUESTION_PERIOD_NOT_ENDED"} | ||
| ) | ||
| ApiResponse<FeedbackRoundDetailResponse> publishAnswers(AdminApiUser apiUser, Long roundId); | ||
| } |
99 changes: 99 additions & 0 deletions
99
...in/java/kr/ac/kookmin/stream/api/admin/welfare/feedback/AdminFeedbackRoundController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| package kr.ac.kookmin.stream.api.admin.welfare.feedback; | ||
|
|
||
| import jakarta.validation.Valid; | ||
| import kr.ac.kookmin.stream.api.admin.AdminApiUser; | ||
| import kr.ac.kookmin.stream.api.admin.welfare.feedback.request.FeedbackAnswerSaveRequest; | ||
| import kr.ac.kookmin.stream.api.admin.welfare.feedback.request.FeedbackRoundCreateRequest; | ||
| import kr.ac.kookmin.stream.api.admin.welfare.feedback.response.FeedbackRoundDetailResponse; | ||
| import kr.ac.kookmin.stream.api.admin.welfare.feedback.response.FeedbackRoundResponse; | ||
| import kr.ac.kookmin.stream.api.admin.welfare.feedback.usecase.AdminFeedbackAnswerUseCase; | ||
| import kr.ac.kookmin.stream.api.common.dto.ApiResponse; | ||
| import kr.ac.kookmin.stream.api.common.dto.PageParams; | ||
| import kr.ac.kookmin.stream.api.common.dto.PageResponse; | ||
| import kr.ac.kookmin.stream.common.PageResult; | ||
| import kr.ac.kookmin.stream.welfare.domain.feedback.domain.FeedbackRoundStatus; | ||
| import kr.ac.kookmin.stream.welfare.domain.feedback.domain.FeedbackRoundSummary; | ||
| import kr.ac.kookmin.stream.welfare.domain.feedback.service.AdminFeedbackService; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.ModelAttribute; | ||
| import org.springframework.web.bind.annotation.PatchMapping; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| 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 | ||
| @RequestMapping("/v1/admin/feedback/rounds") | ||
| @RequiredArgsConstructor | ||
| public class AdminFeedbackRoundController implements AdminFeedbackRoundApi { | ||
|
|
||
| private final AdminFeedbackService adminFeedbackService; | ||
| private final AdminFeedbackAnswerUseCase adminFeedbackAnswerUseCase; | ||
|
|
||
| @Override | ||
| @GetMapping("/current") | ||
| public ApiResponse<FeedbackRoundResponse> getCurrentRound(AdminApiUser apiUser) { | ||
| // 진행 중 회차가 없으면 data를 null로 내려 화면이 "진행 중인 회차 없음"을 그린다 | ||
| return ApiResponse.success( | ||
| adminFeedbackService.getCurrentRound().map(FeedbackRoundResponse::from).orElse(null)); | ||
| } | ||
|
|
||
| @Override | ||
| @GetMapping | ||
| public ApiResponse<PageResponse<FeedbackRoundResponse>> getRounds( | ||
| AdminApiUser apiUser, | ||
| @RequestParam(required = false) Integer year, | ||
| @RequestParam(required = false) String status, | ||
| @Valid @ModelAttribute PageParams pageParams | ||
| ) { | ||
| FeedbackRoundStatus roundStatus = FeedbackRoundStatus.from(status); | ||
| PageResult<FeedbackRoundSummary> result = | ||
| adminFeedbackService.getRounds(year, roundStatus, pageParams.toOffset()); | ||
|
|
||
| return ApiResponse.success(PageResponse.from(result, FeedbackRoundResponse::from)); | ||
| } | ||
|
|
||
| @Override | ||
| @PostMapping | ||
| public ApiResponse<FeedbackRoundResponse> createRound( | ||
| AdminApiUser apiUser, | ||
| @Valid @RequestBody FeedbackRoundCreateRequest request | ||
| ) { | ||
| FeedbackRoundSummary created = adminFeedbackService.createRound(request.opensOn(), request.closesOn()); | ||
| return ApiResponse.success(FeedbackRoundResponse.from(created)); | ||
| } | ||
|
|
||
| @Override | ||
| @GetMapping("/{roundId}") | ||
| public ApiResponse<FeedbackRoundDetailResponse> getRoundAnswers( | ||
| AdminApiUser apiUser, | ||
| @PathVariable Long roundId | ||
| ) { | ||
| return ApiResponse.success( | ||
| FeedbackRoundDetailResponse.from(adminFeedbackAnswerUseCase.getRoundAnswers(roundId))); | ||
| } | ||
|
|
||
| @Override | ||
| @PatchMapping("/{roundId}/answers") | ||
| public ApiResponse<FeedbackRoundDetailResponse> saveAnswers( | ||
| AdminApiUser apiUser, | ||
| @PathVariable Long roundId, | ||
| @Valid @RequestBody FeedbackAnswerSaveRequest request | ||
| ) { | ||
| return ApiResponse.success(FeedbackRoundDetailResponse.from( | ||
| adminFeedbackAnswerUseCase.saveAnswers(roundId, request.toCommands(), apiUser.userId()))); | ||
| } | ||
|
|
||
| @Override | ||
| @PostMapping("/{roundId}/answers/publish") | ||
| public ApiResponse<FeedbackRoundDetailResponse> publishAnswers( | ||
| AdminApiUser apiUser, | ||
| @PathVariable Long roundId | ||
| ) { | ||
| return ApiResponse.success( | ||
| FeedbackRoundDetailResponse.from(adminFeedbackAnswerUseCase.publishAnswers(roundId))); | ||
| } | ||
| } |
57 changes: 57 additions & 0 deletions
57
...va/kr/ac/kookmin/stream/api/admin/welfare/feedback/request/FeedbackAnswerSaveRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package kr.ac.kookmin.stream.api.admin.welfare.feedback.request; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import jakarta.validation.Valid; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.NotEmpty; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import jakarta.validation.constraints.Size; | ||
| import java.util.List; | ||
| import kr.ac.kookmin.stream.welfare.domain.feedback.domain.FeedbackAnswerCommand; | ||
|
|
||
| /** | ||
| * 답변 임시저장 요청. 화면이 질문 여러 개를 체크해 각각 답변을 쓰는 형태라 회차 단위 일괄로 받는다. | ||
| * 보낸 것만 반영하고, 이미 답변이 있는 질문을 다시 보내면 덮어쓴다. | ||
| */ | ||
| public record FeedbackAnswerSaveRequest( | ||
|
|
||
| @Schema(description = "답변 목록. 최소 1건") | ||
| @NotEmpty(message = "답변을 1건 이상 입력해 주세요.") | ||
| List<@Valid @NotNull(message = "답변을 입력해 주세요.") Item> answers | ||
| ) { | ||
|
|
||
| public List<FeedbackAnswerCommand> toCommands() { | ||
| validateFeedbackIdsNotDuplicated(); | ||
|
|
||
| return answers.stream() | ||
| .map(item -> FeedbackAnswerCommand.of(item.feedbackId(), item.answer())) | ||
| .toList(); | ||
| } | ||
|
|
||
| // 같은 질문이 두 번 오면 어느 답변이 남을지 요청만 보고 알 수 없다. | ||
| // IllegalArgumentException → GlobalExceptionHandler가 INVALID_INPUT(400)으로 응답한다 | ||
| private void validateFeedbackIdsNotDuplicated() { | ||
| long distinctFeedbackIdCount = answers.stream() | ||
| .map(Item::feedbackId) | ||
| .distinct() | ||
| .count(); | ||
| if (distinctFeedbackIdCount != answers.size()) { | ||
| throw new IllegalArgumentException("같은 질문의 답변이 중복되었습니다."); | ||
| } | ||
| } | ||
|
|
||
| public record Item( | ||
|
|
||
| @Schema(description = "답변할 피드백 아이디", example = "21") | ||
| @NotNull(message = "답변할 피드백을 선택해 주세요.") | ||
| Long feedbackId, | ||
|
|
||
| // answer는 TEXT 컬럼(최대 65,535바이트)이다. 글자당 최대 4바이트로 잡아도 넘치지 않는 길이로 막는다 | ||
| @Schema(description = "답변 내용. `**텍스트**`로 감싼 부분은 굵게 렌더링하기로 한 약속이다", | ||
| example = "시험 기간에는 열람실을 **24시간** 개방합니다.") | ||
| @NotBlank(message = "답변 내용은 필수 입력값입니다.") | ||
| @Size(max = 16000, message = "답변 내용은 16000자 이하로 입력해 주세요.") | ||
| String answer | ||
| ) { | ||
| } | ||
| } | ||
18 changes: 18 additions & 0 deletions
18
...a/kr/ac/kookmin/stream/api/admin/welfare/feedback/request/FeedbackRoundCreateRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package kr.ac.kookmin.stream.api.admin.welfare.feedback.request; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import java.time.LocalDate; | ||
|
|
||
| // 화면이 입력받는 값은 질문 접수 기간 하나뿐이다. 연도·회차 번호는 서버가 배정한다 | ||
| public record FeedbackRoundCreateRequest( | ||
|
|
||
| @Schema(description = "질문 접수 시작일", example = "2026-10-16") | ||
| @NotNull(message = "질문 접수 시작일과 마감일을 모두 입력해 주세요.") | ||
| LocalDate opensOn, | ||
|
|
||
| @Schema(description = "질문 접수 마감일", example = "2026-10-24") | ||
| @NotNull(message = "질문 접수 시작일과 마감일을 모두 입력해 주세요.") | ||
| LocalDate closesOn | ||
| ) { | ||
| } |
33 changes: 33 additions & 0 deletions
33
.../java/kr/ac/kookmin/stream/api/admin/welfare/feedback/response/AdminFeedbackResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package kr.ac.kookmin.stream.api.admin.welfare.feedback.response; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import kr.ac.kookmin.stream.api.admin.welfare.feedback.usecase.MemberFeedback; | ||
| import kr.ac.kookmin.stream.welfare.domain.feedback.domain.Feedback; | ||
|
|
||
| // 현재 회차 질문 탭의 한 줄. memberName은 탈퇴(소프트 삭제)한 회원의 질문이면 null이다 | ||
| public record AdminFeedbackResponse( | ||
| Long feedbackId, | ||
| int year, | ||
| int round, | ||
| String question, | ||
| String memberName, | ||
| LocalDateTime questionedAt, | ||
| String answer, | ||
| LocalDateTime answeredAt | ||
| ) { | ||
|
|
||
| public static AdminFeedbackResponse from(MemberFeedback memberFeedback) { | ||
| Feedback feedback = memberFeedback.feedback(); | ||
|
|
||
| return new AdminFeedbackResponse( | ||
| feedback.getId(), | ||
| feedback.getYear(), | ||
| feedback.getRound(), | ||
| feedback.getQuestion(), | ||
| memberFeedback.memberName(), | ||
| feedback.getQuestionedAt(), | ||
| feedback.getAnswer(), | ||
| feedback.getAnsweredAt() | ||
| ); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
[a]같은feedbackId가 두 번 오면 둘 다 저장되고 나중 것이 남아요. 화면에서 그럴 일은 드물겠지만 요청만 보고는 어느 답변이 남을지 알 수 없어요.toCommands()안에서validateFeedbackIdsNotDuplicated()로 막아두면 좋을 것 같아요(MemberSignUpRequest의 약관 중복 검사와 같은 방식이에요).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.
fe9f24b 수정했습니다!