-
Notifications
You must be signed in to change notification settings - Fork 0
feat(admin): 관리자 API 16종 및 수험표 PDF 발급 구현 (#22) #73
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
base: develop
Are you sure you want to change the base?
The head ref may contain hidden characters: "22-admin-\uB3C4\uBA54\uC778-\uAC1C\uBC1C"
Changes from all commits
f8e886e
a64488c
9dc3936
14affc1
a5c553d
b98c208
c1d95b2
f40295a
c13f88b
0fa347a
ed041c9
57dcc47
877d815
66c05d0
bda4847
7a8e6b0
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,44 @@ | ||
| package hs.kr.entrydsm.admin.adapterin.web | ||
|
|
||
| import hs.kr.entrydsm.admin.domain.enum.ErrorCode | ||
| import hs.kr.entrydsm.admin.domain.exception.AdminDomainException | ||
| import jakarta.servlet.http.HttpServletRequest | ||
| import jakarta.servlet.http.HttpServletResponse | ||
| import org.springframework.stereotype.Component | ||
| import org.springframework.web.servlet.HandlerInterceptor | ||
|
|
||
| const val USER_ID_HEADER = "X-User-Id" | ||
| const val USER_ROLE_HEADER = "X-User-Role" | ||
| private const val ADMIN_ROLE = "ADMIN" | ||
|
|
||
| /** | ||
| * 관리자 권한을 확인합니다. | ||
| * | ||
| * 공통 규약 7항대로 인증은 Gateway 또는 Identity가 처리하고, 이 서비스는 그들이 넣어 준 | ||
| * 헤더만 신뢰합니다. 클라이언트가 직접 넣은 헤더가 그대로 들어오지 않도록 Gateway에서 | ||
| * 반드시 덮어써야 합니다. | ||
| * | ||
| * ponytail: Gateway가 없는 동안은 이 헤더가 유일한 관문이다. Gateway가 붙으면 | ||
| * 그쪽 JWT 검증으로 옮기고 여기서는 역할 확인만 남긴다. | ||
| */ | ||
| @Component | ||
| class AdminAuthorizationInterceptor : HandlerInterceptor { | ||
|
|
||
| override fun preHandle( | ||
| request: HttpServletRequest, | ||
| response: HttpServletResponse, | ||
| handler: Any, | ||
| ): Boolean { | ||
| val userId = request.getHeader(USER_ID_HEADER) | ||
| val role = request.getHeader(USER_ROLE_HEADER) | ||
|
|
||
| if (userId.isNullOrBlank() || role.isNullOrBlank()) { | ||
| throw AdminDomainException(ErrorCode.AUTH_UNAUTHORIZED) | ||
| } | ||
| if (role != ADMIN_ROLE) { | ||
| throw AdminDomainException(ErrorCode.ACCESS_DENIED) | ||
| } | ||
|
|
||
| return true | ||
| } | ||
|
Comment on lines
+24
to
+43
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. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift Gateway가 없는 상태에서 헤더만으로 권한을 판단합니다. 서비스는 Gateway 도입 전까지 다음 중 하나를 적용하세요.
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package hs.kr.entrydsm.admin.adapterin.web | ||
|
|
||
| /** | ||
| * 관리자 API 경로 상수입니다. | ||
| * | ||
| * Notion 명세의 경로를 그대로 쓰되, 명세에 있던 오타(슬래시 중복, `damin`)와 | ||
| * 수험표만 `v1`이던 버전 불일치는 바로잡았습니다. | ||
| */ | ||
| object AdminEndpointPaths { | ||
| const val BASE = "/api/v11/admin" | ||
|
|
||
| const val APPLICANTS = "$BASE/applicants" | ||
| const val APPLICANT = "$APPLICANTS/{applicantId}" | ||
| const val APPLICANT_ARRIVAL = "$APPLICANT/arrival" | ||
| const val APPLICANT_STATUS = "$APPLICANT/status" | ||
| const val APPLICANT_ADMISSION_TICKET = "$APPLICANT/admission-ticket" | ||
| const val APPLICANT_APPLICATION_DOCUMENT = "$APPLICANT/application-document" | ||
|
|
||
| const val EXAMINEE_NUMBER_ISSUE = "$BASE/examinee-numbers/issue" | ||
| const val SCORE_POLICY = "$BASE/score-policy" | ||
| const val FIRST_SCREENING_RESULTS = "$BASE/screenings/first/results" | ||
| const val FINAL_SCREENING_RESULTS = "$BASE/screenings/final/results" | ||
| const val STATISTICS = "$BASE/statistics" | ||
| const val EXPORTS = "$BASE/exports" | ||
| const val EXPORT = "$EXPORTS/{exportJobId}" | ||
| const val NOTICES = "$BASE/notices" | ||
| const val QUESTION_ANSWERS = "$BASE/questions/{questionId}/answers" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| package hs.kr.entrydsm.admin.adapterin.web | ||
|
|
||
| import hs.kr.entrydsm.admin.adapterin.web.dto.common.ApiResponse | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.common.toDetailResponse | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.common.toResponse | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.common.toSummaryResponse | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.request.UpdateApplicantStatusRequest | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.request.UpdateArrivalRequest | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.response.ApplicantDetailResponse | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.response.ApplicantSummaryResponse | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.response.DownloadResponse | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.response.ExamineeNumberIssueResponse | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.response.PageResponse | ||
| import hs.kr.entrydsm.admin.domain.command.UpdateApplicantStatusCommand | ||
| import hs.kr.entrydsm.admin.domain.command.UpdateArrivalCommand | ||
| import hs.kr.entrydsm.admin.domain.enum.AdmissionType | ||
| import hs.kr.entrydsm.admin.domain.enum.ApplicantStatus | ||
| import hs.kr.entrydsm.admin.domain.enum.GraduationStatus | ||
| import hs.kr.entrydsm.admin.domain.enum.Region | ||
| import hs.kr.entrydsm.admin.domain.model.ApplicantFilter | ||
| import hs.kr.entrydsm.admin.domain.model.PageRequest | ||
| import hs.kr.entrydsm.admin.domain.port.`in`.IssueAdmissionTicketUseCase | ||
| import hs.kr.entrydsm.admin.domain.port.`in`.IssueApplicationDocumentUseCase | ||
| import hs.kr.entrydsm.admin.domain.port.`in`.IssueExamineeNumberUseCase | ||
| import hs.kr.entrydsm.admin.domain.port.`in`.ReadApplicantUseCase | ||
| import hs.kr.entrydsm.admin.domain.port.`in`.UpdateApplicantUseCase | ||
| import jakarta.validation.Valid | ||
| import org.springframework.http.ResponseEntity | ||
| import org.springframework.web.bind.annotation.GetMapping | ||
| 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.RequestParam | ||
| import org.springframework.web.bind.annotation.RestController | ||
|
|
||
| @RestController | ||
| class ApplicantController( | ||
| private val readApplicantUseCase: ReadApplicantUseCase, | ||
| private val updateApplicantUseCase: UpdateApplicantUseCase, | ||
| private val issueExamineeNumberUseCase: IssueExamineeNumberUseCase, | ||
| private val issueAdmissionTicketUseCase: IssueAdmissionTicketUseCase, | ||
| private val issueApplicationDocumentUseCase: IssueApplicationDocumentUseCase, | ||
| ) { | ||
|
|
||
| @GetMapping(AdminEndpointPaths.APPLICANTS) | ||
| fun search( | ||
| @RequestParam(required = false) keyword: String?, | ||
| @RequestParam(required = false) regions: Set<Region>?, | ||
| @RequestParam(required = false) admissionTypes: Set<AdmissionType>?, | ||
| @RequestParam(required = false) graduationStatuses: Set<GraduationStatus>?, | ||
| @RequestParam(required = false) isSubmitted: Boolean?, | ||
| @RequestParam(required = false) statuses: Set<ApplicantStatus>?, | ||
| @RequestParam(defaultValue = "1") page: Int, | ||
| @RequestParam(defaultValue = "10") size: Int, | ||
| ): ResponseEntity<ApiResponse<PageResponse<ApplicantSummaryResponse>>> { | ||
| val result = readApplicantUseCase.search( | ||
| ApplicantFilter( | ||
| keyword = keyword, | ||
| regions = regions.orEmpty(), | ||
| admissionTypes = admissionTypes.orEmpty(), | ||
| graduationStatuses = graduationStatuses.orEmpty(), | ||
| isSubmitted = isSubmitted, | ||
| statuses = statuses.orEmpty(), | ||
| ), | ||
| PageRequest(page = page, size = size), | ||
| ) | ||
|
|
||
| return ResponseEntity.ok( | ||
| ApiResponse(data = result.toResponse { it.toSummaryResponse() }), | ||
| ) | ||
| } | ||
|
|
||
| @GetMapping(AdminEndpointPaths.APPLICANT) | ||
| fun findById( | ||
| @PathVariable applicantId: Long, | ||
| ): ResponseEntity<ApiResponse<ApplicantDetailResponse>> = | ||
| ResponseEntity.ok( | ||
| ApiResponse(data = readApplicantUseCase.findById(applicantId).toDetailResponse()), | ||
| ) | ||
|
|
||
| @PatchMapping(AdminEndpointPaths.APPLICANT_ARRIVAL) | ||
| fun updateArrival( | ||
| @PathVariable applicantId: Long, | ||
| @Valid @RequestBody request: UpdateArrivalRequest, | ||
| ): ResponseEntity<Unit> { | ||
| updateApplicantUseCase.updateArrival( | ||
| UpdateArrivalCommand(applicantId = applicantId, isSubmitted = request.isSubmitted!!), | ||
| ) | ||
| return ResponseEntity.noContent().build() | ||
| } | ||
|
|
||
| @PatchMapping(AdminEndpointPaths.APPLICANT_STATUS) | ||
| fun updateStatus( | ||
| @PathVariable applicantId: Long, | ||
| @Valid @RequestBody request: UpdateApplicantStatusRequest, | ||
| ): ResponseEntity<Unit> { | ||
| updateApplicantUseCase.updateStatus( | ||
| UpdateApplicantStatusCommand( | ||
| applicantId = applicantId, | ||
| status = request.status!!, | ||
| force = request.force, | ||
| reason = request.reason, | ||
| ), | ||
| ) | ||
| return ResponseEntity.noContent().build() | ||
| } | ||
|
|
||
| @PostMapping(AdminEndpointPaths.EXAMINEE_NUMBER_ISSUE) | ||
| fun issueExamineeNumbers(): ResponseEntity<ApiResponse<ExamineeNumberIssueResponse>> = | ||
| ResponseEntity.ok(ApiResponse(data = issueExamineeNumberUseCase.issueAll().toResponse())) | ||
|
|
||
| @GetMapping(AdminEndpointPaths.APPLICANT_ADMISSION_TICKET) | ||
| fun issueAdmissionTicket( | ||
| @PathVariable applicantId: Long, | ||
| ): ResponseEntity<ApiResponse<DownloadResponse>> = | ||
| ResponseEntity.ok( | ||
| ApiResponse( | ||
| data = issueAdmissionTicketUseCase.issueAdmissionTicket(applicantId).toResponse(), | ||
| ), | ||
| ) | ||
|
|
||
| @GetMapping(AdminEndpointPaths.APPLICANT_APPLICATION_DOCUMENT) | ||
| fun issueApplicationDocument( | ||
| @PathVariable applicantId: Long, | ||
| ): ResponseEntity<ApiResponse<DownloadResponse>> = | ||
| ResponseEntity.ok( | ||
| ApiResponse( | ||
| data = issueApplicationDocumentUseCase | ||
| .issueApplicationDocument(applicantId) | ||
| .toResponse(), | ||
| ), | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| package hs.kr.entrydsm.admin.adapterin.web | ||
|
|
||
| import hs.kr.entrydsm.admin.adapterin.web.dto.common.ApiResponse | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.common.toResponse | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.request.EvaluateScreeningRequest | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.request.UpdateScorePolicyRequest | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.response.ScorePolicyResponse | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.response.ScreeningResultResponse | ||
| import hs.kr.entrydsm.admin.adapterin.web.dto.response.StatisticsResponse | ||
| import hs.kr.entrydsm.admin.domain.command.EvaluateScreeningCommand | ||
| import hs.kr.entrydsm.admin.domain.command.UpdateScorePolicyCommand | ||
| import hs.kr.entrydsm.admin.domain.enum.StatisticsMetric | ||
| import hs.kr.entrydsm.admin.domain.model.ScoreWeights | ||
| import hs.kr.entrydsm.admin.domain.port.`in`.EvaluateFinalScreeningUseCase | ||
| import hs.kr.entrydsm.admin.domain.port.`in`.EvaluateFirstScreeningUseCase | ||
| import hs.kr.entrydsm.admin.domain.port.`in`.ReadScorePolicyUseCase | ||
| import hs.kr.entrydsm.admin.domain.port.`in`.ReadStatisticsUseCase | ||
| import hs.kr.entrydsm.admin.domain.port.`in`.UpdateScorePolicyUseCase | ||
| import jakarta.validation.Valid | ||
| import org.springframework.http.ResponseEntity | ||
| import org.springframework.web.bind.annotation.GetMapping | ||
| import org.springframework.web.bind.annotation.PostMapping | ||
| import org.springframework.web.bind.annotation.PutMapping | ||
| import org.springframework.web.bind.annotation.RequestBody | ||
| import org.springframework.web.bind.annotation.RequestHeader | ||
| import org.springframework.web.bind.annotation.RequestParam | ||
| import org.springframework.web.bind.annotation.RestController | ||
|
|
||
| @RestController | ||
| class ScreeningController( | ||
| private val readScorePolicyUseCase: ReadScorePolicyUseCase, | ||
| private val updateScorePolicyUseCase: UpdateScorePolicyUseCase, | ||
| private val evaluateFirstScreeningUseCase: EvaluateFirstScreeningUseCase, | ||
| private val evaluateFinalScreeningUseCase: EvaluateFinalScreeningUseCase, | ||
| private val readStatisticsUseCase: ReadStatisticsUseCase, | ||
| ) { | ||
|
|
||
| @GetMapping(AdminEndpointPaths.SCORE_POLICY) | ||
| fun findCurrentPolicy(): ResponseEntity<ApiResponse<ScorePolicyResponse>> = | ||
| ResponseEntity.ok(ApiResponse(data = readScorePolicyUseCase.findCurrent().toResponse())) | ||
|
|
||
| @PutMapping(AdminEndpointPaths.SCORE_POLICY) | ||
| fun updatePolicy( | ||
| @RequestHeader(USER_ID_HEADER) userId: String, | ||
| @Valid @RequestBody request: UpdateScorePolicyRequest, | ||
| ): ResponseEntity<Unit> { | ||
| val weights = request.weights!! | ||
| updateScorePolicyUseCase.update( | ||
| UpdateScorePolicyCommand( | ||
| weights = ScoreWeights( | ||
| subject = weights.subject!!, | ||
| attendance = weights.attendance!!, | ||
| volunteer = weights.volunteer!!, | ||
| ), | ||
| roundingScale = request.roundingScale!!, | ||
| recalculate = request.recalculate, | ||
| updatedBy = userId, | ||
| ), | ||
| ) | ||
| return ResponseEntity.accepted().build() | ||
| } | ||
|
|
||
| @PostMapping(AdminEndpointPaths.FIRST_SCREENING_RESULTS) | ||
| fun evaluateFirst( | ||
| @RequestBody(required = false) request: EvaluateScreeningRequest?, | ||
| ): ResponseEntity<ApiResponse<ScreeningResultResponse>> = | ||
| ResponseEntity.ok( | ||
| ApiResponse( | ||
| data = evaluateFirstScreeningUseCase | ||
| .evaluateFirst(EvaluateScreeningCommand(dryRun = request?.dryRun ?: false)) | ||
| .toResponse(), | ||
| ), | ||
| ) | ||
|
|
||
| @PostMapping(AdminEndpointPaths.FINAL_SCREENING_RESULTS) | ||
| fun evaluateFinal( | ||
| @RequestBody(required = false) request: EvaluateScreeningRequest?, | ||
| ): ResponseEntity<ApiResponse<ScreeningResultResponse>> = | ||
| ResponseEntity.ok( | ||
| ApiResponse( | ||
| data = evaluateFinalScreeningUseCase | ||
| .evaluateFinal(EvaluateScreeningCommand(dryRun = request?.dryRun ?: false)) | ||
| .toResponse(), | ||
| ), | ||
| ) | ||
|
|
||
| @GetMapping(AdminEndpointPaths.STATISTICS) | ||
| fun statistics( | ||
| @RequestParam metrics: Set<StatisticsMetric>, | ||
| ): ResponseEntity<ApiResponse<StatisticsResponse>> = | ||
| ResponseEntity.ok(ApiResponse(data = readStatisticsUseCase.collect(metrics).toResponse())) | ||
| } |
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.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
공개 매크로에 Starlark docstring을 추가하십시오.
setup_jpa_noarg_plugin은 공개 함수이지만 첫 문장 docstring이 없습니다. 현재 주석은 Starlark 문서화 도구에서 함수 문서로 처리되지 않습니다.수정 예시
As per path instructions,
**/*.bzl: “Keep file/module docstrings and docstrings for public functions/macros”.📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Path instructions