├── settings.gradle.kts ├── docs └── image │ ├── view1.png │ ├── view2.png │ ├── view3.png │ └── view4.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── kotlin │ │ └── app │ │ │ └── askresume │ │ │ ├── global │ │ │ ├── resolver │ │ │ │ ├── token │ │ │ │ │ ├── TokenDto.kt │ │ │ │ │ ├── AccessToken.kt │ │ │ │ │ ├── RefreshToken.kt │ │ │ │ │ ├── AccessTokenResolver.kt │ │ │ │ │ └── RefreshTokenResolver.kt │ │ │ │ └── memberinfo │ │ │ │ │ ├── MemberInfoResolver.kt │ │ │ │ │ ├── MemberInfo.kt │ │ │ │ │ └── MemberInfoArgumentResolver.kt │ │ │ ├── annotation │ │ │ │ └── UseCase.kt │ │ │ ├── jwt │ │ │ │ ├── constant │ │ │ │ │ ├── GrantType.kt │ │ │ │ │ └── JwtTokenType.kt │ │ │ │ ├── exception │ │ │ │ │ └── JwtClaimNotExistsException.kt │ │ │ │ └── dto │ │ │ │ │ └── JwtResponse.kt │ │ │ ├── util │ │ │ │ ├── LoggerUtil.kt │ │ │ │ ├── DateTimeUtils.kt │ │ │ │ └── UriUtil.kt │ │ │ ├── config │ │ │ │ ├── CacheConfig.kt │ │ │ │ ├── jpa │ │ │ │ │ ├── AuditingConfig.kt │ │ │ │ │ └── AuditorAwareImpl.kt │ │ │ │ ├── QuerydslConfig.kt │ │ │ │ ├── ScheduledConfig.kt │ │ │ │ ├── SecurityConfig.kt │ │ │ │ ├── JasyptConfig.kt │ │ │ │ ├── FeignConfiguration.kt │ │ │ │ └── OpenApiConfig.kt │ │ │ ├── error │ │ │ │ ├── exception │ │ │ │ │ ├── BusinessException.kt │ │ │ │ │ ├── NotValidTokenException.kt │ │ │ │ │ ├── ForbiddenAdminException.kt │ │ │ │ │ ├── TokenExpiredException.kt │ │ │ │ │ └── NotAccessTokenTypeException.kt │ │ │ │ ├── FeignClientExceptionErrorDecode.kt │ │ │ │ ├── ErrorResponse.kt │ │ │ │ └── ErrorCodes.kt │ │ │ ├── cookie │ │ │ │ ├── CookieOption.kt │ │ │ │ ├── exception │ │ │ │ │ └── CookieNotFoundException.kt │ │ │ │ ├── CookieProvider.kt │ │ │ │ └── CookieProviderImpl.kt │ │ │ ├── security │ │ │ │ └── HtmlCharacterEscapes.kt │ │ │ ├── interceptor │ │ │ │ ├── AdminAuthorizationInterceptor.kt │ │ │ │ └── AuthenticationInterceptor.kt │ │ │ └── filter │ │ │ │ └── MdcLoggingFilter.kt │ │ │ ├── domain │ │ │ ├── prompt │ │ │ │ ├── constant │ │ │ │ │ └── PromptType.kt │ │ │ │ ├── exception │ │ │ │ │ └── PromptNotFoundException.kt │ │ │ │ ├── repository │ │ │ │ │ └── PromptRepository.kt │ │ │ │ ├── model │ │ │ │ │ └── Prompt.kt │ │ │ │ └── service │ │ │ │ │ └── PromptReadOnlyService.kt │ │ │ ├── submit │ │ │ │ ├── constant │ │ │ │ │ ├── Satisfaction.kt │ │ │ │ │ ├── SubmitDataStatus.kt │ │ │ │ │ ├── SubmitStatus.kt │ │ │ │ │ └── ServiceType.kt │ │ │ │ ├── repository │ │ │ │ │ ├── expression │ │ │ │ │ │ └── SubmitExpression.kt │ │ │ │ │ ├── SubmitDataRepository.kt │ │ │ │ │ └── SubmitRepository.kt │ │ │ │ ├── exception │ │ │ │ │ ├── SubmitNotFoundException.kt │ │ │ │ │ ├── SubmitDataNotFoundException.kt │ │ │ │ │ ├── ContentLengthLackException.kt │ │ │ │ │ ├── ContentLengthOverException.kt │ │ │ │ │ ├── UnauthorizedSubmitAccessException.kt │ │ │ │ │ └── StatusIsNotCompletedException.kt │ │ │ │ ├── dto │ │ │ │ │ └── SubmitQueryDtos.kt │ │ │ │ ├── mapper │ │ │ │ │ └── SubmitDataMapper.kt │ │ │ │ ├── model │ │ │ │ │ ├── SubmitData.kt │ │ │ │ │ └── Submit.kt │ │ │ │ └── service │ │ │ │ │ ├── SubmitDataCommandService.kt │ │ │ │ │ └── SubmitReadOnlyService.kt │ │ │ ├── job │ │ │ │ ├── repository │ │ │ │ │ ├── JobRepository.kt │ │ │ │ │ ├── JobMasterRepository.kt │ │ │ │ │ └── JobDataRepositoryQuery.kt │ │ │ │ ├── dto │ │ │ │ │ └── JobDtos.kt │ │ │ │ ├── exception │ │ │ │ │ └── JobNotFoundException.kt │ │ │ │ ├── model │ │ │ │ │ ├── JobMaster.kt │ │ │ │ │ └── Job.kt │ │ │ │ └── service │ │ │ │ │ ├── JobReadOnlyService.kt │ │ │ │ │ └── JobCommandService.kt │ │ │ ├── member │ │ │ │ ├── constant │ │ │ │ │ ├── Role.kt │ │ │ │ │ └── MemberType.kt │ │ │ │ ├── exception │ │ │ │ │ ├── RefreshTokenExpiredException.kt │ │ │ │ │ ├── RefreshTokenNotFoundException.kt │ │ │ │ │ ├── MemberNotFoundException.kt │ │ │ │ │ └── DuplicateMemberException.kt │ │ │ │ ├── dto │ │ │ │ │ └── MemberDtos.kt │ │ │ │ ├── repository │ │ │ │ │ ├── expression │ │ │ │ │ │ └── MemberExpression.kt │ │ │ │ │ ├── MemberRepository.kt │ │ │ │ │ └── MemberQueryRepository.kt │ │ │ │ └── service │ │ │ │ │ ├── MemberReadOnlyService.kt │ │ │ │ │ └── MemberCommandService.kt │ │ │ ├── result │ │ │ │ ├── repository │ │ │ │ │ └── ResultRepository.kt │ │ │ │ ├── service │ │ │ │ │ └── ResultService.kt │ │ │ │ └── model │ │ │ │ │ └── Result.kt │ │ │ ├── generative │ │ │ │ ├── interview │ │ │ │ │ ├── repository │ │ │ │ │ │ ├── InterviewMakerRepository.kt │ │ │ │ │ │ └── InterviewMakerQueryRepository.kt │ │ │ │ │ ├── exception │ │ │ │ │ │ └── DifficultyNotExistsException.kt │ │ │ │ │ ├── constant │ │ │ │ │ │ ├── ResumeType.kt │ │ │ │ │ │ └── DifficultyType.kt │ │ │ │ │ ├── service │ │ │ │ │ │ ├── InterviewMakerReadOnlyService.kt │ │ │ │ │ │ └── InterviewMakerCommandService.kt │ │ │ │ │ ├── dto │ │ │ │ │ │ └── InterviewMakerDtos.kt │ │ │ │ │ └── model │ │ │ │ │ │ └── ResultInterviewMaker.kt │ │ │ │ ├── service │ │ │ │ │ └── GenerativeCommandService.kt │ │ │ │ └── factory │ │ │ │ │ └── GenerativeCommandFactory.kt │ │ │ ├── locale │ │ │ │ ├── validator │ │ │ │ │ └── LocaleValidator.kt │ │ │ │ └── constant │ │ │ │ │ └── LocaleType.kt │ │ │ ├── manager │ │ │ │ ├── exception │ │ │ │ │ └── NotPermittedContentTypeException.kt │ │ │ │ ├── validator │ │ │ │ │ └── PdfManagerValidator.kt │ │ │ │ └── service │ │ │ │ │ └── PdfManagerService.kt │ │ │ └── AuditingEntity.kt │ │ │ ├── external │ │ │ └── openai │ │ │ │ ├── constant │ │ │ │ └── OpenAiRole.kt │ │ │ │ ├── client │ │ │ │ └── OpenAiClient.kt │ │ │ │ ├── service │ │ │ │ └── OpenAiService.kt │ │ │ │ ├── mapper │ │ │ │ └── OpenAiMapper.kt │ │ │ │ └── dto │ │ │ │ └── OpenAiDtos.kt │ │ │ ├── oauth │ │ │ ├── constant │ │ │ │ ├── OAuthGrantType.kt │ │ │ │ ├── OAuthQueryParam.kt │ │ │ │ └── OAuthProvider.kt │ │ │ ├── userinfo │ │ │ │ ├── OAuthUserInfo.kt │ │ │ │ ├── OAuthUserInfoFactory.kt │ │ │ │ ├── GoogleUserInfo.kt │ │ │ │ └── LinkedInUserInfo.kt │ │ │ ├── client │ │ │ │ ├── OAuthEmailInfoClient.kt │ │ │ │ ├── OAuthUserInfoClient.kt │ │ │ │ └── OAuthTokenClient.kt │ │ │ ├── exception │ │ │ │ ├── UnsupportedProviderException.kt │ │ │ │ └── CannotReadOAuthUserInfoException.kt │ │ │ ├── service │ │ │ │ └── OAuthService.kt │ │ │ ├── dto │ │ │ │ └── OAuthResponse.kt │ │ │ └── OAuthProperties.kt │ │ │ ├── api │ │ │ ├── job │ │ │ │ ├── vo │ │ │ │ │ ├── JobVo.kt │ │ │ │ │ └── AdminJobVo.kt │ │ │ │ ├── mapper │ │ │ │ │ └── JobMapper.kt │ │ │ │ ├── usecase │ │ │ │ │ ├── JobUseCase.kt │ │ │ │ │ └── AdminJobUseCase.kt │ │ │ │ └── controller │ │ │ │ │ ├── AdminJobController.kt │ │ │ │ │ └── JobController.kt │ │ │ ├── ApiResult.kt │ │ │ ├── extract │ │ │ │ ├── vo │ │ │ │ │ └── ExtracteVo.kt │ │ │ │ ├── usecase │ │ │ │ │ └── ExtractUseCase.kt │ │ │ │ └── controller │ │ │ │ │ └── ExtractController.kt │ │ │ ├── PageResponse.kt │ │ │ ├── member │ │ │ │ ├── mapper │ │ │ │ │ └── MyMemberMapper.kt │ │ │ │ ├── usecase │ │ │ │ │ └── MyMemberUseCase.kt │ │ │ │ ├── vo │ │ │ │ │ └── MyMemberVo.kt │ │ │ │ └── controller │ │ │ │ │ └── MyMemberController.kt │ │ │ ├── generative │ │ │ │ ├── mapper │ │ │ │ │ └── InterviewMakerMapper.kt │ │ │ │ └── controller │ │ │ │ │ └── InterviewMakerController.kt │ │ │ ├── access │ │ │ │ └── usecase │ │ │ │ │ ├── AccessUseCase.kt │ │ │ │ │ └── OauthUseCase.kt │ │ │ └── submit │ │ │ │ ├── vo │ │ │ │ └── SubmitVo.kt │ │ │ │ ├── mapper │ │ │ │ └── SubmitMapper.kt │ │ │ │ ├── usecase │ │ │ │ └── SubmitUseCase.kt │ │ │ │ └── controller │ │ │ │ └── SubmitController.kt │ │ │ ├── AskResumeApplication.kt │ │ │ └── scheduler │ │ │ └── ScheduleTasks.kt │ └── resources │ │ ├── templates │ │ ├── index.html │ │ └── error │ │ │ ├── 404.html │ │ │ └── 500.html │ │ ├── application-test.yml │ │ ├── application-local.yml │ │ ├── application-staging.yml │ │ ├── application-prod.yml │ │ ├── message │ │ ├── messages_ko.properties │ │ └── messages_en.properties │ │ └── lucy-xss-superset-sax.xml └── test │ └── kotlin │ └── app │ └── askresume │ ├── AskResumeApplicationTests.kt │ ├── fixture │ ├── JobFixture.kt │ ├── ModifyInfoRequestFixture.kt │ ├── SaveJobRequestFixture.kt │ ├── SubmitFixture.kt │ ├── InformationRequestFixture.kt │ └── MemberFixture.kt │ ├── ValidationUtils.kt │ ├── RepositoryTest.kt │ ├── domain │ ├── submit │ │ ├── constant │ │ │ └── ServiceTypeTest.kt │ │ ├── model │ │ │ └── SubmitTest.kt │ │ └── service │ │ │ └── SubmitCommandServiceTest.kt │ ├── locale │ │ └── constant │ │ │ └── LocaleTypeTest.kt │ ├── job │ │ ├── repository │ │ │ └── JobMasterRepositoryKtTest.kt │ │ └── service │ │ │ ├── JobCommandServiceMockkTest.kt │ │ │ ├── JobReadOnlyServiceMockkOnlyTest.kt │ │ │ ├── JobCommandServiceTest.kt │ │ │ ├── JobReadOnlyServiceMockkAndKoTest.kt │ │ │ └── JobReadOnlyServiceTest.kt │ └── result │ │ └── service │ │ └── ResultServiceTest.kt │ ├── api │ ├── generative │ │ └── vo │ │ │ └── InformationRequestTest.kt │ ├── job │ │ ├── usecase │ │ │ ├── AdminJobUseCaseTest.kt │ │ │ ├── JobUseCaseTest.kt │ │ │ └── JobUseCaseMockkTest.kt │ │ └── vo │ │ │ └── SaveJobRequestTest.kt │ └── member │ │ ├── mapper │ │ └── MyMemberMapperTest.kt │ │ └── vo │ │ └── ModifyInfoRequestTest.kt │ ├── JasyptEncryptionTest.kt │ └── RANDOM.kt ├── .gitignore ├── Korean.md ├── README.md └── gradlew.bat /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ask-resume" 2 | -------------------------------------------------------------------------------- /docs/image/view1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ask-resume/ask-resume-backend/HEAD/docs/image/view1.png -------------------------------------------------------------------------------- /docs/image/view2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ask-resume/ask-resume-backend/HEAD/docs/image/view2.png -------------------------------------------------------------------------------- /docs/image/view3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ask-resume/ask-resume-backend/HEAD/docs/image/view3.png -------------------------------------------------------------------------------- /docs/image/view4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ask-resume/ask-resume-backend/HEAD/docs/image/view4.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ask-resume/ask-resume-backend/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/resolver/token/TokenDto.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.resolver.token 2 | 3 | data class TokenDto( 4 | val token: String, 5 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/prompt/constant/PromptType.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.prompt.constant 2 | 3 | enum class PromptType { 4 | INTERVIEW_MAKER, 5 | INTERVIEW_MAKER_PDF, 6 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/annotation/UseCase.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.annotation 2 | 3 | import org.springframework.stereotype.Service 4 | 5 | @Service 6 | annotation class UseCase 7 | -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/AskResumeApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package app.askresume 2 | 3 | import org.springframework.boot.test.context.SpringBootTest; 4 | 5 | @SpringBootTest 6 | class AskResumeApplicationTests -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/jwt/constant/GrantType.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.jwt.constant 2 | 3 | enum class GrantType( 4 | val type: String, 5 | ) { 6 | 7 | BEARER("Bearer") 8 | 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/resolver/token/AccessToken.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.resolver.token 2 | 3 | @Target(AnnotationTarget.VALUE_PARAMETER) 4 | @Retention(AnnotationRetention.RUNTIME) 5 | annotation class AccessToken -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/resolver/token/RefreshToken.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.resolver.token 2 | 3 | @Target(AnnotationTarget.VALUE_PARAMETER) 4 | @Retention(AnnotationRetention.RUNTIME) 5 | annotation class RefreshToken -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/constant/Satisfaction.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.constant 2 | 3 | enum class Satisfaction { 4 | 5 | SATISFIED, // 만족 6 | DISSATISFIED, // 불만족 7 | NO_RESPONSE, // 무응답 8 | 9 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/external/openai/constant/OpenAiRole.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.external.openai.constant 2 | 3 | enum class OpenAiRole( 4 | val value: String, 5 | ) { 6 | 7 | SYSTEM("system"), 8 | USER("user") 9 | 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/resolver/memberinfo/MemberInfoResolver.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.resolver.memberinfo 2 | 3 | @Target(AnnotationTarget.VALUE_PARAMETER) 4 | @Retention(AnnotationRetention.RUNTIME) 5 | annotation class MemberInfoResolver -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/constant/SubmitDataStatus.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.constant 2 | 3 | enum class SubmitDataStatus { 4 | 5 | WAITING, // 대기 6 | RESEND, // 재전송 해야함 7 | SUCCESS, // 성공 8 | ; 9 | 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/constant/SubmitStatus.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.constant 2 | 3 | enum class SubmitStatus { 4 | 5 | WAITING, // 대기중 6 | GENERATING, // 생성중 7 | FAIL, // 생성실패 8 | COMPLETED, // 생성완료 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/util/LoggerUtil.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.util 2 | 3 | import org.slf4j.LoggerFactory 4 | 5 | object LoggerUtil { 6 | inline fun T.logger(): org.slf4j.Logger = LoggerFactory.getLogger(T::class.java) 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/constant/OAuthGrantType.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth.constant 2 | 3 | enum class OAuthGrantType( 4 | val value: String, 5 | ) { 6 | AUTHORIZATION_CODE("authorization_code"), 7 | REFRESH_TOKEN("refresh_token"), 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/job/repository/JobRepository.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.repository 2 | 3 | import app.askresume.domain.job.model.Job 4 | import org.springframework.data.jpa.repository.JpaRepository 5 | 6 | interface JobRepository : JpaRepository -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/config/CacheConfig.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.config 2 | 3 | import org.springframework.cache.annotation.EnableCaching 4 | import org.springframework.context.annotation.Configuration 5 | 6 | @EnableCaching 7 | @Configuration 8 | class CacheConfig -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/member/constant/Role.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.member.constant 2 | 3 | 4 | enum class Role { 5 | 6 | USER, ADMIN; 7 | 8 | companion object { 9 | fun from(role: String): Role = valueOf(role) 10 | } 11 | 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/result/repository/ResultRepository.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.result.repository; 2 | 3 | import app.askresume.domain.result.model.Result 4 | import org.springframework.data.jpa.repository.JpaRepository 5 | 6 | interface ResultRepository : JpaRepository -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/constant/OAuthQueryParam.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth.constant 2 | 3 | enum class OAuthQueryParam( 4 | val key: String, 5 | ) { 6 | 7 | CLIENT_ID("client_id"), 8 | REDIRECT_URI("redirect_uri"), 9 | RESPONSE_TYPE("response_type"), 10 | SCOPE("scope"), 11 | 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/resolver/memberinfo/MemberInfo.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.resolver.memberinfo 2 | 3 | import app.askresume.domain.member.constant.Role 4 | import io.swagger.v3.oas.annotations.Hidden 5 | 6 | @Hidden 7 | data class MemberInfo( 8 | val memberId: Long, 9 | val role: Role, 10 | ) -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/fixture/JobFixture.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.fixture 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.domain.job.model.JobMaster 5 | 6 | object JobFixture { 7 | 8 | fun jobMaster() : JobMaster = JobMaster( 9 | masterName = RANDOM.nextString(3, 15) 10 | ) 11 | 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/repository/expression/SubmitExpression.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.repository.expression 2 | 3 | import app.askresume.domain.submit.model.QSubmit.submit 4 | 5 | object SubmitExpression { 6 | 7 | fun memberIdEq(memberId : Long?) = memberId?.let { submit.member.id.eq(memberId) } 8 | 9 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/ValidationUtils.kt: -------------------------------------------------------------------------------- 1 | package app.askresume 2 | 3 | import javax.validation.ConstraintViolation 4 | import javax.validation.Validation 5 | 6 | object ValidationUtils { 7 | 8 | fun validate(obj: T): MutableSet> = Validation.buildDefaultValidatorFactory().validator 9 | .run { this.validate(obj) } 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/job/dto/JobDtos.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.dto 2 | 3 | import com.querydsl.core.annotations.QueryProjection 4 | import java.time.LocalDateTime 5 | 6 | data class JobDto @QueryProjection constructor( 7 | val id: Long, 8 | val name: String, 9 | val createdAt: LocalDateTime?, 10 | val updatedAt: LocalDateTime?, 11 | ) -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/RepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume 2 | 3 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest 4 | import org.springframework.test.context.ActiveProfiles 5 | 6 | 7 | @Target(AnnotationTarget.CLASS) 8 | @Retention(AnnotationRetention.RUNTIME) 9 | @ActiveProfiles("test") 10 | @DataJpaTest 11 | annotation class RepositoryTest -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/generative/interview/repository/InterviewMakerRepository.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.generative.interview.repository 2 | 3 | import app.askresume.domain.generative.interview.model.ResultInterviewMaker 4 | import org.springframework.data.jpa.repository.JpaRepository 5 | 6 | interface InterviewMakerRepository : JpaRepository -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/jwt/constant/JwtTokenType.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.jwt.constant 2 | 3 | 4 | enum class JwtTokenType( 5 | val cookieName: String, 6 | ) { 7 | 8 | ACCESS("access_token"), 9 | REFRESH("refresh_token"), 10 | ; 11 | 12 | companion object { 13 | fun isAccessToken(tokenType: String) = ACCESS.name == tokenType 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/generative/service/GenerativeCommandService.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.generative.service 2 | 3 | import app.askresume.external.openai.dto.ChoicesDto 4 | 5 | 6 | interface GenerativeCommandService { 7 | 8 | fun saveGenerativeResult( 9 | submitId : Long, 10 | submitDataId : Long, 11 | choices : List 12 | ) 13 | } -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/job/vo/JobVo.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.job.vo 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema 4 | 5 | @Schema(name = "직업 리스트 Response") 6 | data class JobResponse( 7 | @field:Schema(description = "job id", example = "1") 8 | val id: Long, 9 | @field:Schema(description = "직업 영어명", example = "backend developer") 10 | val name: String, 11 | ) 12 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/error/exception/BusinessException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.error.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | 5 | open class BusinessException( 6 | open val codeBook: ErrorCodes, 7 | val properties: String, 8 | val arguments: Array, 9 | override val cause: Throwable? = null, 10 | ) : RuntimeException(codeBook.description, cause) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/ApiResult.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat 4 | import java.time.LocalDateTime 5 | 6 | data class ApiResult( 7 | val data: T, 8 | ) { 9 | 10 | @field:JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") 11 | val timestamp: LocalDateTime = LocalDateTime.now() 12 | 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/error/exception/NotValidTokenException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.error.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | 5 | class NotValidTokenException( 6 | override val cause: Throwable? = null, 7 | ) : BusinessException( 8 | codeBook = ErrorCodes.NOT_VALID_TOKEN, 9 | properties = "not.valid.token", 10 | arguments = arrayOf(), 11 | cause = cause, 12 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/error/exception/ForbiddenAdminException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.error.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | 5 | class ForbiddenAdminException( 6 | override val cause: Throwable? = null, 7 | ) : BusinessException( 8 | codeBook = ErrorCodes.FORBIDDEN_ADMIN, 9 | properties = "forbidden.admin", 10 | arguments = arrayOf(), 11 | cause = cause, 12 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/error/exception/TokenExpiredException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.error.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | 5 | 6 | class TokenExpiredException ( 7 | override val cause: Throwable? = null, 8 | ) : BusinessException( 9 | codeBook = ErrorCodes.TOKEN_EXPIRED, 10 | properties = "token.expired", 11 | arguments = arrayOf(), 12 | cause = cause, 13 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/extract/vo/ExtracteVo.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.extract.vo 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema 4 | 5 | @Schema(name = "PDF TO TEXT 추출 Response") 6 | data class ExtractedTextResponse( 7 | 8 | @field:Schema( 9 | description = "변환된 TEXT", 10 | example = "Hello, I'm backend developer Hong Gildong. Something, something, something..." 11 | ) val text: String, 12 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/cookie/CookieOption.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.cookie 2 | 3 | import java.time.Duration 4 | 5 | data class CookieOption( 6 | val name: String, 7 | val value: String = "", 8 | val httpOnly: Boolean = false, 9 | val secure: Boolean = false, 10 | val domain: String? = null, 11 | val path: String = "/", 12 | val maxAge: Duration? = null, 13 | val sameSite: String? = null, 14 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/locale/validator/LocaleValidator.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.locale.validator 2 | 3 | import app.askresume.domain.locale.constant.LocaleType 4 | import org.springframework.stereotype.Component 5 | 6 | @Component 7 | class LocaleValidator { 8 | 9 | fun validateLocaleType(locale: String): String { 10 | return if (!LocaleType.isLocaleType(locale)) LocaleType.EN.name else locale 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/PageResponse.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api 2 | 3 | import org.springframework.data.domain.Page 4 | 5 | data class PageResponse( 6 | val page: Int, 7 | val pageSize: Int, 8 | val totalElements: Long, 9 | val list: List 10 | ) { 11 | 12 | constructor (page: Page) : this( 13 | page.number, 14 | page.size, 15 | page.totalElements, 16 | page.content, 17 | ) 18 | 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/job/mapper/JobMapper.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.job.mapper 2 | 3 | import app.askresume.api.job.vo.JobResponse 4 | import app.askresume.domain.job.dto.JobDto 5 | 6 | object JobMapper { 7 | 8 | fun jobResponseListOf( 9 | jobDtos: List 10 | ): List = jobDtos.map { dto -> 11 | JobResponse( 12 | id = dto.id, 13 | name = dto.name 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/config/jpa/AuditingConfig.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.config.jpa 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing 6 | 7 | 8 | @Configuration 9 | @EnableJpaAuditing 10 | class AuditingConfig { 11 | 12 | @Bean 13 | fun auditorAware() = AuditorAwareImpl() 14 | 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/userinfo/OAuthUserInfo.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth.userinfo 2 | 3 | import app.askresume.domain.member.constant.MemberType 4 | 5 | abstract class OAuthUserInfo( 6 | protected val attributes: Map, 7 | ) { 8 | 9 | abstract val email: String 10 | abstract val name: String 11 | abstract val profile: String? 12 | abstract val locale: String 13 | abstract val memberType: MemberType 14 | 15 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/fixture/ModifyInfoRequestFixture.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.fixture 2 | 3 | import app.askresume.api.member.vo.ModifyInfoRequest 4 | 5 | object ModifyInfoRequestFixture { 6 | 7 | fun modifyInfoRequest( 8 | username : String = "test kim", 9 | profile : String = "https://domain.com/img_110x110.jpg", 10 | ) = ModifyInfoRequest( 11 | username = username, 12 | profile = profile, 13 | ) 14 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/fixture/SaveJobRequestFixture.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.fixture 2 | 3 | import app.askresume.api.job.vo.SaveJobRequest 4 | 5 | object SaveJobRequestFixture { 6 | 7 | fun saveJobRequest( 8 | englishJobName: String = "backend developer", 9 | koreaJobName: String = "백엔드 개발자", 10 | ) = SaveJobRequest( 11 | englishJobName = englishJobName, 12 | koreaJobName = koreaJobName, 13 | ) 14 | 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/client/OAuthEmailInfoClient.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth.client 2 | 3 | import feign.Headers 4 | import feign.Param 5 | import feign.RequestLine 6 | import java.net.URI 7 | 8 | interface OAuthEmailInfoClient { 9 | 10 | @RequestLine("GET") 11 | @Headers(value = ["Authorization: {token}"]) 12 | fun getEmailInfo( 13 | emailUrl: URI, 14 | @Param("token") token: String, 15 | ): Map 16 | 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/client/OAuthUserInfoClient.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth.client 2 | 3 | import feign.Headers 4 | import feign.Param 5 | import feign.RequestLine 6 | import java.net.URI 7 | 8 | interface OAuthUserInfoClient { 9 | 10 | @RequestLine("GET") 11 | @Headers(value = ["Authorization: {token}"]) 12 | fun getUserInfo( 13 | userInfoUrl: URI, 14 | @Param("token") token: String, 15 | ): MutableMap 16 | 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/error/exception/NotAccessTokenTypeException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.error.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | 5 | open class NotAccessTokenTypeException ( 6 | override val cause: Throwable? = null, 7 | ) : BusinessException( 8 | codeBook = ErrorCodes.NOT_ACCESS_TOKEN_TYPE, 9 | properties = "not.access.token.type", 10 | arguments = arrayOf(), 11 | cause = cause, 12 | ) 13 | 14 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/AskResumeApplication.kt: -------------------------------------------------------------------------------- 1 | package app.askresume 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan 5 | import org.springframework.boot.runApplication 6 | 7 | @SpringBootApplication 8 | @ConfigurationPropertiesScan 9 | class AskResumeApplication 10 | 11 | fun main(args: Array) { 12 | runApplication(*args) 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/job/exception/JobNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class JobNotFoundException( 7 | jobId: Long, 8 | override val cause: Throwable? = null, 9 | ) : BusinessException( 10 | codeBook = ErrorCodes.ENTITY_NOT_FOUND, 11 | properties = "new.job.not.exists", 12 | arguments = arrayOf(jobId), 13 | cause = cause, 14 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/member/constant/MemberType.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.member.constant 2 | 3 | enum class MemberType { 4 | 5 | GOOGLE, 6 | LINKED_IN, 7 | ; 8 | 9 | companion object { 10 | fun from(type: String): MemberType { 11 | return MemberType.valueOf(type.uppercase()) 12 | } 13 | 14 | fun isMemberType(type: String): Boolean { 15 | return MemberType.values().any { it.name == type.uppercase() } 16 | } 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/scheduler/ScheduleTasks.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.scheduler 2 | 3 | import app.askresume.scheduler.job.GenerativeModelJob 4 | import org.springframework.scheduling.annotation.Scheduled 5 | import org.springframework.stereotype.Component 6 | 7 | @Component 8 | class ScheduleTasks( 9 | private val generativeModelJob : GenerativeModelJob 10 | ) { 11 | 12 | // @Scheduled(fixedDelay = 1000) 13 | // fun generativeModelJob() { 14 | // generativeModelJob.execute() 15 | // } 16 | 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/member/exception/RefreshTokenExpiredException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.member.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class RefreshTokenExpiredException( 7 | override val cause: Throwable? = null, 8 | ) : BusinessException( 9 | codeBook = ErrorCodes.REFRESH_TOKEN_EXPIRED, 10 | properties = "refresh.token.expired", 11 | arguments = arrayOf(), 12 | cause = cause, 13 | ) 14 | 15 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/exception/SubmitNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class SubmitNotFoundException( 7 | submitId: Long, 8 | override val cause: Throwable? = null, 9 | ) : BusinessException( 10 | codeBook = ErrorCodes.ENTITY_NOT_FOUND, 11 | properties = "submit.not.exists", 12 | arguments = arrayOf(submitId), 13 | cause = cause, 14 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/manager/exception/NotPermittedContentTypeException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.manager.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class NotPermittedContentTypeException( 7 | override val cause: Throwable? = null, 8 | ) : BusinessException( 9 | codeBook = ErrorCodes.NOT_PERMITTED_CONTENT_TYPE, 10 | properties = "not.permitted.content.type", 11 | arguments = arrayOf(), 12 | cause = cause, 13 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/member/exception/RefreshTokenNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.member.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class RefreshTokenNotFoundException( 7 | override val cause: Throwable? = null, 8 | ) : BusinessException( 9 | codeBook = ErrorCodes.REFRESH_TOKEN_NOT_FOUND, 10 | properties = "refresh.token.not.found", 11 | arguments = arrayOf(), 12 | cause = cause, 13 | ) 14 | 15 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/prompt/exception/PromptNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.prompt.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class PromptNotFoundException( 7 | promptType: String, 8 | override val cause: Throwable? = null, 9 | ) : BusinessException( 10 | codeBook = ErrorCodes.ENTITY_NOT_FOUND, 11 | properties = "prompt.not.exists", 12 | arguments = arrayOf(promptType), 13 | cause = cause, 14 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/cookie/exception/CookieNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.cookie.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class CookieNotFoundException( 7 | cookieName: String, 8 | override val cause: Throwable? = null, 9 | ) : BusinessException( 10 | codeBook = ErrorCodes.ENTITY_NOT_FOUND, 11 | properties = "cookie.not.found", 12 | arguments = arrayOf(cookieName), 13 | cause = cause, 14 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/jwt/exception/JwtClaimNotExistsException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.jwt.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class JwtClaimNotExistsException( 7 | claimName: String, 8 | override val cause: Throwable? = null, 9 | ) : BusinessException( 10 | codeBook = ErrorCodes.NOT_VALID_TOKEN, 11 | properties = "jwt.claim.not.exists", 12 | arguments = arrayOf(claimName), 13 | cause = cause, 14 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/member/exception/MemberNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.member.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class MemberNotFoundException( 7 | memberId: Long, 8 | override val cause: Throwable? = null, 9 | ) : BusinessException( 10 | codeBook = ErrorCodes.ENTITY_NOT_FOUND, 11 | properties = "member.not.exists", 12 | arguments = arrayOf(memberId), 13 | cause = cause, 14 | ) 15 | 16 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/exception/SubmitDataNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class SubmitDataNotFoundException( 7 | submitId: Long, 8 | override val cause: Throwable? = null, 9 | ) : BusinessException( 10 | codeBook = ErrorCodes.ENTITY_NOT_FOUND, 11 | properties = "submit.data.not.exists", 12 | arguments = arrayOf(submitId), 13 | cause = cause, 14 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/exception/UnsupportedProviderException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class UnsupportedProviderException( 7 | providerName: String, 8 | override val cause: Throwable? = null, 9 | ) : BusinessException( 10 | codeBook = ErrorCodes.ENTITY_NOT_FOUND, 11 | properties = "oauth.provider.unsupported", 12 | arguments = arrayOf(providerName), 13 | cause = cause, 14 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/generative/interview/exception/DifficultyNotExistsException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.generative.interview.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class DifficultyNotExistsException( 7 | override val cause: Throwable? = null, 8 | ) : BusinessException( 9 | codeBook = ErrorCodes.ENUM_VALIDATE_NOT_EXIST, 10 | properties = "difficulty.not.exists", 11 | arguments = arrayOf(), 12 | cause = cause, 13 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/service/OAuthService.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth.service 2 | 3 | import app.askresume.oauth.constant.OAuthProvider 4 | import app.askresume.oauth.dto.OAuthResponse 5 | import app.askresume.oauth.userinfo.OAuthUserInfo 6 | import java.net.URI 7 | 8 | interface OAuthService { 9 | 10 | fun authorize(provider: OAuthProvider): URI 11 | 12 | fun getToken(code: String, provider: OAuthProvider): OAuthResponse.Token 13 | 14 | fun getUserInfo(accessToken: String, provider: OAuthProvider): OAuthUserInfo 15 | 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/exception/ContentLengthLackException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | 7 | open class ContentLengthLackException( 8 | contentSize : Int, 9 | override val cause: Throwable? = null, 10 | ) : BusinessException( 11 | codeBook = ErrorCodes.INVALID_CONTENT, 12 | properties = "content.length.lack", 13 | arguments = arrayOf(contentSize), 14 | cause = cause, 15 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/exception/ContentLengthOverException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | 7 | open class ContentLengthOverException( 8 | contentSize : Int, 9 | override val cause: Throwable? = null, 10 | ) : BusinessException( 11 | codeBook = ErrorCodes.INVALID_CONTENT, 12 | properties = "content.length.over", 13 | arguments = arrayOf(contentSize), 14 | cause = cause, 15 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/exception/CannotReadOAuthUserInfoException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class CannotReadOAuthUserInfoException( 7 | providerName: String, 8 | override val cause: Throwable? = null, 9 | ) : BusinessException( 10 | codeBook = ErrorCodes.ENTITY_NOT_FOUND, 11 | properties = "oauth.userinfo.cannot.read", 12 | arguments = arrayOf(providerName), 13 | cause = cause, 14 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/job/repository/JobMasterRepository.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.repository 2 | 3 | import app.askresume.domain.job.exception.JobNotFoundException 4 | import app.askresume.domain.job.model.JobMaster 5 | import org.springframework.data.jpa.repository.JpaRepository 6 | import org.springframework.data.repository.findByIdOrNull 7 | 8 | fun JobMasterRepository.findJobMasterById(id: Long) : JobMaster = 9 | findByIdOrNull(id) ?: throw JobNotFoundException(id) 10 | 11 | interface JobMasterRepository : JpaRepository -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/exception/UnauthorizedSubmitAccessException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class UnauthorizedSubmitAccessException( 7 | submitId: Long, 8 | override val cause: Throwable? = null, 9 | ) : BusinessException( 10 | codeBook = ErrorCodes.UNAUTHORIZED_ACCESS, 11 | properties = "unauthorized.submit.access", 12 | arguments = arrayOf(submitId), 13 | cause = cause, 14 | ) -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/fixture/SubmitFixture.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.fixture 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.domain.member.model.Member 5 | import app.askresume.domain.submit.constant.ServiceType 6 | import app.askresume.domain.submit.model.Submit 7 | 8 | object SubmitFixture { 9 | 10 | fun submit(member: Member): Submit = Submit( 11 | title = RANDOM.nextString(10, 30), 12 | serviceType = ServiceType.INTERVIEW_MAKER, 13 | dataCount = RANDOM.nextInt(1, 5), 14 | member = member, 15 | ) 16 | 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/util/DateTimeUtils.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.util 2 | 3 | import java.time.LocalDateTime 4 | import java.time.ZoneId 5 | import java.util.* 6 | 7 | /** 8 | * Date와 관련된 내용 처리용 Util 9 | * 10 | * @author igor 11 | */ 12 | object DateTimeUtils { 13 | 14 | /** 15 | * Date -> LocalDateTime 으로 형변환 16 | * @param date : LocalDateTime 으로 바꿀 Date 17 | */ 18 | fun convertToLocalDateTime(date: Date): LocalDateTime { 19 | return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime() 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/config/QuerydslConfig.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.config 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import javax.persistence.EntityManager 7 | import javax.persistence.PersistenceContext 8 | 9 | @Configuration 10 | class QuerydslConfig { 11 | 12 | @PersistenceContext 13 | private lateinit var entityManager: EntityManager 14 | 15 | @Bean 16 | fun jpaQueryFactory() = JPAQueryFactory(entityManager) 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/userinfo/OAuthUserInfoFactory.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth.userinfo 2 | 3 | import app.askresume.oauth.constant.OAuthProvider 4 | import org.springframework.stereotype.Component 5 | 6 | @Component 7 | class OAuthUserInfoFactory { 8 | 9 | fun create(provider: OAuthProvider, oAuthAttributes: Map): OAuthUserInfo { 10 | return when (provider) { 11 | OAuthProvider.GOOGLE -> GoogleUserInfo(oAuthAttributes) 12 | OAuthProvider.LINKED_IN -> LinkedInUserInfo(oAuthAttributes) 13 | } 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/userinfo/GoogleUserInfo.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth.userinfo 2 | 3 | import app.askresume.domain.member.constant.MemberType 4 | 5 | class GoogleUserInfo( 6 | attributes: Map, 7 | ) : OAuthUserInfo(attributes) { 8 | 9 | override val email get() = attributes["email"] as String 10 | override val name get() = attributes["name"] as String 11 | override val profile get() = attributes["picture"] as String? 12 | override val locale get() = attributes["locale"] as String 13 | override val memberType get() = MemberType.GOOGLE 14 | 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/member/exception/DuplicateMemberException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.member.exception 2 | 3 | import app.askresume.global.error.ErrorCodes 4 | import app.askresume.global.error.exception.BusinessException 5 | 6 | open class DuplicateMemberException( 7 | email: String, 8 | memberType: String, 9 | override val cause: Throwable? = null, 10 | ) : BusinessException( 11 | codeBook = ErrorCodes.ALREADY_REGISTERED_MEMBER, 12 | properties = "already.registered.member", 13 | arguments = arrayOf(email, memberType), 14 | cause = cause, 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/util/UriUtil.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.util 2 | 3 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder 4 | import java.net.URI 5 | 6 | object UriUtil { 7 | 8 | fun createUri(): URI { 9 | return ServletUriComponentsBuilder.fromCurrentRequest() 10 | .build() 11 | .toUri() 12 | } 13 | 14 | fun createUri(id: Long?): URI { 15 | return ServletUriComponentsBuilder.fromCurrentRequest() 16 | .path("/{id}") 17 | .buildAndExpand(id) 18 | .toUri() 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/locale/constant/LocaleType.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.locale.constant 2 | 3 | 4 | enum class LocaleType( 5 | val value: String 6 | ) { 7 | 8 | KO("Korean"), 9 | EN("English"), 10 | ; 11 | 12 | fun value() = value 13 | 14 | companion object { 15 | fun from(type: String): LocaleType { 16 | return LocaleType.valueOf(type.uppercase()) 17 | } 18 | 19 | fun isLocaleType(type: String): Boolean { 20 | return LocaleType.values().any { it.name == type.uppercase() } 21 | } 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/generative/interview/constant/ResumeType.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.generative.interview.constant 2 | 3 | 4 | enum class ResumeType( 5 | private val value: String 6 | ) { 7 | 8 | INTRODUCTION("introduction"), // 자기소개서 9 | CAREER("career history"), // 경력 10 | TECHNICAL("technology stack"), // 기술스택 11 | PROJECT("project experience"), // 프로젝트 경험 12 | EDUCATION("education history"), // 교육 이력 13 | OUTSIDE_ACTIVITIES("outside activities"), // 대외 활동 14 | AAC("achievements and credentials"), // 자격증,어학,수상내역 15 | ; 16 | 17 | fun value() = value 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/constant/ServiceType.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.constant 2 | 3 | enum class ServiceType( 4 | val model: String, 5 | val maxTokens: Int, 6 | ) { 7 | 8 | INTERVIEW_MAKER( 9 | model = "gpt-3.5-turbo", 10 | maxTokens = 3000, 11 | ), 12 | INTERVIEW_MAKER_PDF( 13 | model = "gpt-3.5-turbo-16k", 14 | maxTokens = 10000, 15 | ), 16 | ; 17 | 18 | 19 | companion object { 20 | fun from(type: String): ServiceType { 21 | return ServiceType.valueOf(type.uppercase()) 22 | } 23 | 24 | } 25 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | .jpb 27 | !**/src/main/**/out/ 28 | !**/src/test/**/out/ 29 | 30 | ### NetBeans ### 31 | /nbproject/private/ 32 | /nbbuild/ 33 | /dist/ 34 | /nbdist/ 35 | /.nb-gradle/ 36 | 37 | ### VS Code ### 38 | .vscode/ 39 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/dto/OAuthResponse.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth.dto 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | class OAuthResponse { 6 | 7 | data class Token( 8 | @JsonProperty("access_token") 9 | val accessToken: String, 10 | @JsonProperty("expires_in") 11 | val expiresIn: Long, 12 | @JsonProperty("refresh_token") 13 | val refreshToken: String?, 14 | @JsonProperty("refresh_token_expires_in") 15 | val refreshTokenExpiresIn: Long?, 16 | @JsonProperty("scope") 17 | val scope: String, 18 | ) 19 | 20 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/domain/submit/constant/ServiceTypeTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.constant 2 | 3 | import org.assertj.core.api.Assertions 4 | import org.junit.jupiter.api.DisplayName 5 | import org.junit.jupiter.api.Test 6 | 7 | class ServiceTypeTest { 8 | 9 | @Test 10 | fun `String Type의 ServiceType 이름을 전달하면 ServiceType enum을 반환한다`() { 11 | // given 12 | val type = "INTERVIEW_MAKER" 13 | 14 | // when 15 | val from = ServiceType.from(type) 16 | 17 | // then 18 | Assertions.assertThat(from).isEqualTo(ServiceType.INTERVIEW_MAKER) 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/exception/StatusIsNotCompletedException.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.exception 2 | 3 | import app.askresume.domain.submit.constant.SubmitStatus 4 | import app.askresume.global.error.ErrorCodes 5 | import app.askresume.global.error.exception.BusinessException 6 | 7 | open class SubmitStatusIsNotCompletedException( 8 | status: SubmitStatus, 9 | override val cause: Throwable? = null, 10 | ) : BusinessException( 11 | codeBook = ErrorCodes.STATUS_IS_NOT_COMPLETED, 12 | properties = "submit.is.not.completed", 13 | arguments = arrayOf(status), 14 | cause = cause, 15 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/member/dto/MemberDtos.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.member.dto 2 | 3 | import app.askresume.domain.member.constant.MemberType 4 | import app.askresume.domain.member.constant.Role 5 | import com.querydsl.core.annotations.QueryProjection 6 | import java.time.LocalDateTime 7 | 8 | data class MemberInfoDto @QueryProjection constructor( 9 | val memberId: Long, 10 | val email: String, 11 | val memberType: MemberType, 12 | val locale: String, 13 | val role: Role, 14 | val username: String, 15 | val profile: String?, 16 | val refreshToken: String?, 17 | val tokenExpirationTime: LocalDateTime? 18 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/job/usecase/JobUseCase.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.job.usecase 2 | 3 | import app.askresume.api.job.mapper.JobMapper 4 | import app.askresume.api.job.vo.JobResponse 5 | import app.askresume.domain.job.service.JobReadOnlyService 6 | import app.askresume.domain.locale.constant.LocaleType 7 | import app.askresume.global.annotation.UseCase 8 | 9 | @UseCase 10 | class JobUseCase( 11 | private val jobReadOnlyService: JobReadOnlyService, 12 | ) { 13 | 14 | fun findJobs(locale: LocaleType): List { 15 | val jobs = jobReadOnlyService.findJobs(locale) 16 | return JobMapper.jobResponseListOf(jobs) 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/prompt/repository/PromptRepository.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.prompt.repository 2 | 3 | import app.askresume.domain.prompt.constant.PromptType 4 | import app.askresume.domain.prompt.exception.PromptNotFoundException 5 | import app.askresume.domain.prompt.model.Prompt 6 | import org.springframework.data.jpa.repository.JpaRepository 7 | 8 | fun PromptRepository.findPromptByPromptType(promptType: PromptType) : Prompt = 9 | findByPromptType(promptType) ?: throw PromptNotFoundException(promptType.name) 10 | 11 | interface PromptRepository : JpaRepository { 12 | fun findByPromptType(promptType: PromptType): Prompt? 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/api/generative/vo/InformationRequestTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.generative.vo 2 | 3 | import app.askresume.ValidationUtils 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.junit.jupiter.api.Test 6 | import app.askresume.fixture.InformationRequestFixture 7 | 8 | class InformationRequestTest { 9 | 10 | @Test 11 | fun `경력이 30년을 초과하면 에러가 발생한다`() { 12 | // given 13 | val request = InformationRequestFixture.informationRequest(careerYear = 31) 14 | 15 | // when 16 | val validate = ValidationUtils.validate(request) 17 | 18 | // then 19 | assertThat(validate).isNotEmpty() 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/config/ScheduledConfig.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.config 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.scheduling.TaskScheduler 6 | import org.springframework.scheduling.annotation.EnableScheduling 7 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler 8 | 9 | 10 | @Configuration 11 | @EnableScheduling 12 | class ScheduledConfig { 13 | 14 | @Bean 15 | fun scheduler(): TaskScheduler { 16 | val scheduler = ThreadPoolTaskScheduler() 17 | scheduler.poolSize = 4 18 | return scheduler 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/job/vo/AdminJobVo.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.job.vo 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema 4 | import org.hibernate.validator.constraints.Length 5 | import javax.validation.constraints.NotBlank 6 | 7 | @Schema(name = "어드민 직업정보 등록 Request") 8 | data class SaveJobRequest( 9 | @field:NotBlank 10 | @field:Length(max = 150) 11 | @field:Schema(description = "직업 영어명", example = "backend developer", required = true) 12 | val englishJobName: String, 13 | 14 | @field:NotBlank 15 | @field:Length(max = 150) 16 | @field:Schema(description = "직업 한글명", example = "백엔드 개발자", required = true) 17 | val koreaJobName: String, 18 | ) 19 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/member/mapper/MyMemberMapper.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.member.mapper 2 | 3 | import app.askresume.api.member.vo.MemberInfoResponse 4 | import app.askresume.domain.member.dto.MemberInfoDto 5 | 6 | object MyMemberMapper { 7 | 8 | fun memberInfoResponseOf(memberInfoDto: MemberInfoDto): MemberInfoResponse { 9 | return MemberInfoResponse( 10 | memberId = memberInfoDto.memberId, 11 | email = memberInfoDto.email, 12 | username = memberInfoDto.username, 13 | profile = memberInfoDto.profile, 14 | role = memberInfoDto.role, 15 | locale = memberInfoDto.locale 16 | ) 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/member/repository/expression/MemberExpression.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.member.repository.expression 2 | 3 | import app.askresume.domain.member.constant.MemberType 4 | import app.askresume.domain.member.model.QMember.member 5 | 6 | 7 | object MemberExpression { 8 | 9 | fun memberIdEq(memberId: Long?) = memberId?.let { member.id.eq(memberId) } 10 | 11 | fun emailEq(email: String?) = email?.let { member.email.eq(email) } 12 | 13 | fun memberTypeEq(memberType: MemberType?) = memberType?.let { member.memberType.eq(memberType) } 14 | 15 | fun refreshTokenEq(refreshToken: String?) = refreshToken?.let { member.refreshToken.eq(refreshToken) } 16 | 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/generative/interview/constant/DifficultyType.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.generative.interview.constant 2 | 3 | import java.util.* 4 | 5 | 6 | enum class DifficultyType( 7 | private val value: String 8 | ) { 9 | 10 | EASY("easy"), 11 | MEDIUM("medium"), 12 | HARD("hard") 13 | ; 14 | 15 | fun value() = value 16 | 17 | companion object { 18 | fun isDifficultyType(type: String): Boolean { 19 | val difficultyTypes = Arrays.stream(values()) 20 | .filter { difficultyType -> difficultyType.name.equals(type, ignoreCase = true) }.toList() 21 | return difficultyTypes.size != 0 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/extract/usecase/ExtractUseCase.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.extract.usecase 2 | 3 | import app.askresume.api.extract.vo.ExtractedTextResponse 4 | import app.askresume.domain.manager.service.PdfManagerService 5 | import app.askresume.global.annotation.UseCase 6 | import app.askresume.global.util.LoggerUtil.logger 7 | import org.springframework.web.multipart.MultipartFile 8 | 9 | @UseCase 10 | class ExtractUseCase( 11 | private val pdfManagerService: PdfManagerService 12 | ) { 13 | 14 | val log = logger() 15 | 16 | fun pdfToText(file: MultipartFile): ExtractedTextResponse { 17 | return ExtractedTextResponse( 18 | pdfManagerService.pdfToText(file) 19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/job/usecase/AdminJobUseCase.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.job.usecase 2 | 3 | import app.askresume.api.job.vo.SaveJobRequest 4 | import app.askresume.domain.job.service.JobCommandService 5 | import app.askresume.global.annotation.UseCase 6 | import org.springframework.transaction.annotation.Transactional 7 | 8 | @UseCase 9 | @Transactional(readOnly = true) 10 | class AdminJobUseCase( 11 | private val jobCommandService: JobCommandService 12 | ) { 13 | 14 | @Transactional 15 | fun save(request: SaveJobRequest) { 16 | jobCommandService.saveJobs( 17 | englishJobName = request.englishJobName, 18 | koreaJobName = request.koreaJobName, 19 | ) 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/jwt/dto/JwtResponse.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.jwt.dto 2 | 3 | import app.askresume.global.jwt.constant.JwtTokenType 4 | import com.fasterxml.jackson.annotation.JsonFormat 5 | import java.util.* 6 | 7 | class JwtResponse { 8 | 9 | data class Token( 10 | val grantType: String, 11 | val tokenType: JwtTokenType, 12 | val token: String, 13 | val expirationTime: Long, 14 | 15 | @field:JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") 16 | val expireDate: Date, 17 | ) 18 | 19 | data class TokenSet( 20 | val accessToken: Token, 21 | val refreshToken: Token, 22 | ) 23 | 24 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/fixture/InformationRequestFixture.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.fixture 2 | 3 | import app.askresume.api.generative.vo.InformationRequest 4 | import app.askresume.domain.generative.interview.constant.DifficultyType 5 | import app.askresume.domain.locale.constant.LocaleType 6 | 7 | object InformationRequestFixture { 8 | 9 | fun informationRequest( 10 | jobId : Long = 1L, 11 | difficulty : DifficultyType = DifficultyType.MEDIUM, 12 | careerYear : Int = 3, 13 | language : LocaleType = LocaleType.EN, 14 | ) = InformationRequest( 15 | jobId = jobId, 16 | difficulty = difficulty, 17 | careerYear = careerYear, 18 | language = language, 19 | ) 20 | } -------------------------------------------------------------------------------- /src/main/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:h2:mem:test 4 | username: sa 5 | driver-class-name: org.h2.Driver 6 | h2: 7 | console: 8 | enabled: true 9 | path: /h2-console 10 | jpa: 11 | hibernate: 12 | ddl-auto: create 13 | defer-datasource-initialization: false 14 | sql: 15 | init: 16 | mode: never 17 | 18 | logging: 19 | level: 20 | app.askresume: debug 21 | 22 | # JWT Token 설정 23 | token: 24 | secret: TEST 25 | access-token-expiration-time: 900000 # 15분 1000(ms) x 60(s) x 15(m) 26 | refresh-token-expiration-time: 1209600000 # 2주 1000(ms) x 60 (s) x 60(m) x 24(h) x 14(d) 27 | 28 | # Jasypt password 설정 29 | jasypt: 30 | encryptor: 31 | password: TEST -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/client/OAuthTokenClient.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth.client 2 | 3 | import app.askresume.oauth.dto.OAuthResponse 4 | import feign.Headers 5 | import feign.Param 6 | import feign.RequestLine 7 | import java.net.URI 8 | 9 | interface OAuthTokenClient { 10 | 11 | @RequestLine("POST ?grant_type={grantType}&code={code}&client_id={clientId}&client_secret={clientSecret}&redirect_uri={redirectUri}") 12 | @Headers("Content-Type: application/json") 13 | fun getToken( 14 | tokenUrl: URI, 15 | @Param grantType: String, 16 | @Param code: String, 17 | @Param clientId: String, 18 | @Param clientSecret: String, 19 | @Param redirectUri: String, 20 | ): OAuthResponse.Token 21 | 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/generative/interview/service/InterviewMakerReadOnlyService.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.generative.interview.service 2 | 3 | import app.askresume.domain.generative.interview.dto.InterviewMakerDto 4 | import app.askresume.domain.generative.interview.repository.InterviewMakerQueryRepository 5 | import org.springframework.stereotype.Service 6 | import org.springframework.transaction.annotation.Transactional 7 | 8 | @Service 9 | @Transactional(readOnly = true) 10 | class InterviewMakerReadOnlyService( 11 | private val interviewMakerQueryRepository: InterviewMakerQueryRepository 12 | ) { 13 | 14 | fun findInterviewMaker(submitId: Long): List { 15 | return interviewMakerQueryRepository.findQueryInterviewMaker(submitId) 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/manager/validator/PdfManagerValidator.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.manager.validator 2 | 3 | import app.askresume.domain.manager.exception.NotPermittedContentTypeException 4 | import org.springframework.beans.factory.annotation.Value 5 | import org.springframework.stereotype.Component 6 | 7 | @Component 8 | class PdfManagerValidator { 9 | 10 | @Value("\${file.licensed.content}") 11 | private lateinit var licensedContentTypes: List 12 | 13 | fun validateContentType(contentType: String?) { 14 | val contentTypes = licensedContentTypes.stream() 15 | .filter { type -> !type.equals(contentType, ignoreCase = true) }.toList() 16 | 17 | if (contentTypes.size != 0) 18 | throw NotPermittedContentTypeException() 19 | } 20 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/fixture/MemberFixture.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.fixture 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.domain.member.constant.MemberType 5 | import app.askresume.domain.member.constant.Role 6 | import app.askresume.domain.member.model.Member 7 | 8 | object MemberFixture { 9 | 10 | fun user(): Member = Member( 11 | email = RANDOM.nextEmail(), 12 | memberType = MemberType.GOOGLE, 13 | locale = "kr", 14 | role = Role.USER, 15 | username = RANDOM.nextString(4, 20), 16 | ) 17 | 18 | fun admin(): Member = Member( 19 | email = RANDOM.nextEmail(), 20 | memberType = MemberType.GOOGLE, 21 | locale = "kr", 22 | role = Role.ADMIN, 23 | username = RANDOM.nextString(4, 20), 24 | ) 25 | 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/job/model/JobMaster.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.model 2 | 3 | import app.askresume.domain.BaseEntity 4 | import org.hibernate.annotations.Comment 5 | import org.hibernate.annotations.SQLDelete 6 | import org.hibernate.annotations.Where 7 | import javax.persistence.* 8 | 9 | 10 | @Where(clause = "is_deleted = false") 11 | @SQLDelete(sql = "UPDATE job_master SET is_deleted = true WHERE id = ?") 12 | @Entity 13 | class JobMaster( 14 | masterName: String, 15 | 16 | @Comment(value = "id") 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | var id: Long? = null 20 | ) : BaseEntity() { 21 | 22 | @Comment(value = "작업마스터명") 23 | @Column(length = 150, nullable = false) 24 | var masterName: String = masterName 25 | protected set 26 | 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/OAuthProperties.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth 2 | 3 | import app.askresume.oauth.constant.OAuthProvider 4 | import org.springframework.boot.context.properties.ConfigurationProperties 5 | import org.springframework.boot.context.properties.ConstructorBinding 6 | 7 | @ConstructorBinding 8 | @ConfigurationProperties(prefix = "oauth") 9 | data class OAuthProperties( 10 | val domain: String, 11 | val clientHome: String, 12 | val providers: Map 13 | ) 14 | 15 | data class OAuthProviderProperties( 16 | val clientId: String, 17 | val clientSecret: String, 18 | val authorizationUrl: String, 19 | val userInfoUrl: String, 20 | val tokenUrl: String, 21 | val redirectUrl: String, 22 | val emailUrl: String?, 23 | val scope: List?, 24 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/repository/SubmitDataRepository.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.repository 2 | 3 | import app.askresume.domain.submit.constant.SubmitDataStatus 4 | import app.askresume.domain.submit.exception.SubmitDataNotFoundException 5 | import app.askresume.domain.submit.model.Submit 6 | import app.askresume.domain.submit.model.SubmitData 7 | import org.springframework.data.jpa.repository.JpaRepository 8 | import org.springframework.data.repository.findByIdOrNull 9 | 10 | fun SubmitDataRepository.findSubmitDataById(id: Long): SubmitData = 11 | findByIdOrNull(id) ?: throw SubmitDataNotFoundException(id) 12 | 13 | 14 | interface SubmitDataRepository : JpaRepository { 15 | 16 | fun countBySubmitAndSubmitDataStatus(submit: Submit, submitDataStatus: SubmitDataStatus) : Int 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/config/SecurityConfig.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.config 2 | 3 | import app.askresume.global.jwt.service.TokenManager 4 | import org.springframework.beans.factory.annotation.Value 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | 8 | @Configuration 9 | class SecurityConfig { 10 | 11 | @Value("\${token.access-token-expiration-time}") 12 | private lateinit var accessTokenExpirationTime: String 13 | 14 | @Value("\${token.refresh-token-expiration-time}") 15 | private lateinit var refreshTokenExpirationTime: String 16 | 17 | @Value("\${token.secret}") 18 | private lateinit var tokenSecret: String 19 | 20 | @Bean 21 | fun tokenManager(): TokenManager { 22 | return TokenManager(accessTokenExpirationTime, refreshTokenExpirationTime, tokenSecret) 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/config/JasyptConfig.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.config 2 | 3 | import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties 4 | import org.jasypt.encryption.pbe.PooledPBEStringEncryptor 5 | import org.springframework.beans.factory.annotation.Value 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | 9 | @Configuration 10 | @EnableEncryptableProperties 11 | class JasyptConfig { 12 | 13 | @Value("\${jasypt.encryptor.password}") 14 | private lateinit var password: String 15 | 16 | @Bean 17 | fun jasyptStringEncryptor(): PooledPBEStringEncryptor { 18 | val encryptor = PooledPBEStringEncryptor() 19 | encryptor.setPoolSize(4) 20 | encryptor.setPassword(password) 21 | encryptor.setAlgorithm("PBEWithMD5AndTripleDES") 22 | return encryptor 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/cookie/CookieProvider.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.cookie 2 | 3 | import app.askresume.global.jwt.dto.JwtResponse 4 | import org.springframework.http.ResponseCookie 5 | import javax.servlet.http.Cookie 6 | 7 | interface CookieProvider { 8 | 9 | /** 10 | * 새 쿠키를 만듭니다. 11 | */ 12 | fun createCookie(cookieOption: CookieOption): ResponseCookie 13 | 14 | /** 15 | * JWT 토큰 정보로 쿠키를 만듭니다. 16 | * @return JWT 토큰 Cookie 17 | * @param tokenDto JWT 토큰 정보가 담긴 DTO 18 | * @param domain 쿠키가 발급될 도메인 정보 19 | */ 20 | fun createTokenCookie(tokenDto: JwtResponse.Token, domain: String): ResponseCookie 21 | 22 | /** 23 | * 쿠키 목록에서 특정 쿠키를 가져옵니다. 24 | * @return Cookie (찾는 쿠키가 없으면 null) 25 | * @param cookies 쿠키 목록 정보 26 | * @param cookieName 가져올 쿠키의 이름 27 | */ 28 | fun getCookie(cookies: Array?, cookieName: String): Cookie? 29 | 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/repository/SubmitRepository.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.repository 2 | 3 | import app.askresume.domain.member.model.Member 4 | import app.askresume.domain.submit.exception.SubmitNotFoundException 5 | import app.askresume.domain.submit.exception.UnauthorizedSubmitAccessException 6 | import app.askresume.domain.submit.model.Submit 7 | import org.springframework.data.jpa.repository.JpaRepository 8 | import org.springframework.data.repository.findByIdOrNull 9 | 10 | fun SubmitRepository.findSubmitById(id: Long): Submit = findByIdOrNull(id) 11 | ?: throw SubmitNotFoundException(id) 12 | 13 | fun SubmitRepository.existsSubmitByIdAndMember(id: Long, member: Member) = 14 | if(existsByIdAndMember(id, member)) null 15 | else throw UnauthorizedSubmitAccessException(id) 16 | 17 | interface SubmitRepository : JpaRepository { 18 | 19 | fun existsByIdAndMember(id: Long, member: Member): Boolean 20 | 21 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/JasyptEncryptionTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume 2 | 3 | import org.jasypt.encryption.pbe.PooledPBEStringEncryptor 4 | import org.junit.jupiter.api.Test 5 | 6 | // 해당 부분 내용 추가 후 Commit 금지 7 | class JasyptEncryptionTest { 8 | @Test 9 | fun jasyptTest() { 10 | val password = "1111" // 아무거나 적어놔야 test 됨 11 | val encryptor = PooledPBEStringEncryptor() 12 | encryptor.setPoolSize(4) 13 | encryptor.setPassword(password) 14 | encryptor.setAlgorithm(Companion.ALGORITHM_NAME) 15 | 16 | val content = "ask_resume" // 암호화 할 내용 17 | val encryptedContent = encryptor.encrypt(content) // 암호화 18 | val decryptedContent = encryptor.decrypt(encryptedContent) // 복호화 19 | 20 | println("Enc : $encryptedContent") 21 | println("Dec: $decryptedContent") 22 | } 23 | 24 | companion object { 25 | private const val ALGORITHM_NAME: String = "PBEWithMD5AndTripleDES" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/member/repository/MemberRepository.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.member.repository 2 | 3 | import app.askresume.domain.member.constant.MemberType 4 | import app.askresume.domain.member.exception.DuplicateMemberException 5 | import app.askresume.domain.member.exception.MemberNotFoundException 6 | import app.askresume.domain.member.model.Member 7 | import org.springframework.data.jpa.repository.JpaRepository 8 | import org.springframework.data.repository.findByIdOrNull 9 | 10 | fun MemberRepository.findMemberById(id: Long) = 11 | findByIdOrNull(id) ?: throw MemberNotFoundException(id) 12 | 13 | fun MemberRepository.validateDuplicateMember(email: String, memberType: MemberType): Boolean = 14 | if (existsByEmailAndMemberType(email, memberType)) throw DuplicateMemberException(email, memberType.name) 15 | else false 16 | 17 | interface MemberRepository : JpaRepository { 18 | 19 | fun existsByEmailAndMemberType(email: String, memberType: MemberType): Boolean 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/result/service/ResultService.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.result.service 2 | 3 | import app.askresume.domain.result.model.Result 4 | import app.askresume.domain.result.repository.ResultRepository 5 | import org.springframework.stereotype.Service 6 | import org.springframework.transaction.annotation.Transactional 7 | 8 | @Service 9 | @Transactional(readOnly = true) 10 | class ResultService( 11 | private val resultRepository: ResultRepository, 12 | ) { 13 | 14 | @Transactional 15 | fun saveResultResponseData( 16 | model: String, 17 | created: Long, 18 | promptTokens: Int, 19 | contentToken: Int, 20 | totalTokens: Int 21 | ) { 22 | 23 | resultRepository.save( 24 | Result( 25 | model = model, 26 | created = created, 27 | promptTokens = promptTokens, 28 | contentToken = contentToken, 29 | totalTokens = totalTokens, 30 | ) 31 | ) 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/dto/SubmitQueryDtos.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.dto 2 | 3 | import app.askresume.domain.member.constant.MemberType 4 | import app.askresume.domain.submit.constant.ServiceType 5 | import app.askresume.domain.submit.constant.SubmitStatus 6 | import com.querydsl.core.annotations.QueryProjection 7 | import java.time.LocalDateTime 8 | 9 | 10 | data class SubmitDto @QueryProjection constructor( 11 | val memberId : Long, 12 | val email : String, 13 | val memberType : MemberType, 14 | val submitId : Long, 15 | val title: String, 16 | val dataCount: Int, 17 | val attempts: Int, 18 | val submitStatus: SubmitStatus, 19 | val serviceType: ServiceType, 20 | val createdAt : LocalDateTime, 21 | val updatedAt : LocalDateTime?, 22 | ) 23 | 24 | 25 | data class FirstSubmittedDto @QueryProjection constructor( 26 | val submitId: Long, 27 | val submitDataId: Long, 28 | val serviceType: ServiceType, 29 | val parameter: Map = mapOf(), 30 | ) 31 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/external/openai/client/OpenAiClient.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.external.openai.client 2 | 3 | import app.askresume.external.openai.dto.ChatCompletionsMessageResponse 4 | import app.askresume.external.openai.dto.ChatCompletionsRequest 5 | import app.askresume.global.config.FeignConfiguration 6 | import org.springframework.cloud.openfeign.FeignClient 7 | import org.springframework.http.HttpHeaders 8 | import org.springframework.web.bind.annotation.PostMapping 9 | import org.springframework.web.bind.annotation.RequestBody 10 | import org.springframework.web.bind.annotation.RequestHeader 11 | 12 | @FeignClient(name = "open-ai", url = "\${external.openai.url}", configuration = [FeignConfiguration::class]) 13 | interface OpenAiClient { 14 | 15 | @PostMapping("/v1/chat/completions", consumes = ["application/json"]) 16 | fun createChatCompletion( 17 | @RequestHeader(HttpHeaders.AUTHORIZATION) accessToken: String, 18 | @RequestBody request: ChatCompletionsRequest 19 | ) : ChatCompletionsMessageResponse 20 | 21 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/domain/submit/model/SubmitTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.model 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.domain.submit.constant.SubmitStatus 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.junit.jupiter.api.Test 7 | 8 | class SubmitTest { 9 | 10 | @Test 11 | fun `제출건의 상태를 변경하면, 변경한 상태값이 저장된다`() { 12 | // given 13 | val changeStatus = RANDOM.nextObject(SubmitStatus::class) 14 | val submit = RANDOM.nextObject(Submit::class) 15 | 16 | // when 17 | submit.updateStatus(changeStatus) 18 | 19 | // then 20 | assertThat(submit.submitStatus).isEqualTo(changeStatus) 21 | } 22 | 23 | @Test 24 | fun `제출한 주문건의 재시도 횟수를 1증가 시킨다`() { 25 | // given 26 | val submit = RANDOM.nextObject(Submit::class) 27 | val incrementedValues = submit.attempts + 1 28 | 29 | // when 30 | submit.increaseAttempts() 31 | 32 | // then 33 | assertThat(submit.attempts).isEqualTo(incrementedValues) 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/main/resources/application-local.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:h2:mem:test 4 | username: sa 5 | driver-class-name: org.h2.Driver 6 | h2: 7 | console: 8 | enabled: true 9 | path: /h2-console 10 | jpa: 11 | hibernate: 12 | ddl-auto: create 13 | defer-datasource-initialization: true 14 | sql: 15 | init: 16 | mode: embedded 17 | 18 | logging: 19 | level: 20 | app.askresume: debug 21 | 22 | # JWT Token 설정 // 개발환경 인증 편하게 하기 위해 23 | token: 24 | access-token-expiration-time: 9999999999999 25 | refresh-token-expiration-time: 9999999999999 26 | 27 | oauth: 28 | domain: localhost 29 | client-home: http://localhost:4000 30 | providers: 31 | google: 32 | redirect-url: http://localhost:8080/api/oauth/google/callback 33 | linked-in: 34 | redirect-url: http://localhost:8080/api/oauth/linked-in/callback 35 | 36 | # Actuator Config 37 | management: 38 | endpoints: 39 | web: 40 | exposure: 41 | include: 42 | - "health" 43 | - "metrics" 44 | - "prometheus" -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/external/openai/service/OpenAiService.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.external.openai.service 2 | 3 | import app.askresume.external.openai.client.OpenAiClient 4 | import app.askresume.external.openai.dto.ChatCompletionsMessageResponse 5 | import app.askresume.external.openai.dto.ChatCompletionsRequest 6 | import app.askresume.global.jwt.constant.GrantType 7 | import org.springframework.beans.factory.annotation.Value 8 | import org.springframework.stereotype.Service 9 | import org.springframework.transaction.annotation.Transactional 10 | 11 | @Service 12 | @Transactional(readOnly = true) 13 | class OpenAiService( 14 | private val openAiClient: OpenAiClient, 15 | ) { 16 | 17 | @Value("\${external.openai.token}") 18 | private lateinit var token: String 19 | 20 | fun requestOpenAiChatCompletion( 21 | request: ChatCompletionsRequest 22 | ): ChatCompletionsMessageResponse { 23 | return openAiClient.createChatCompletion( 24 | accessToken = "${GrantType.BEARER.type} $token", 25 | request = request 26 | ) 27 | } 28 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/api/job/usecase/AdminJobUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.job.usecase 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.api.job.vo.SaveJobRequest 5 | import app.askresume.domain.job.service.JobCommandService 6 | import org.junit.jupiter.api.Test 7 | import org.junit.jupiter.api.extension.ExtendWith 8 | import org.mockito.BDDMockito.then 9 | import org.mockito.InjectMocks 10 | import org.mockito.Mock 11 | import org.mockito.junit.jupiter.MockitoExtension 12 | 13 | @ExtendWith(MockitoExtension::class) 14 | class AdminJobUseCaseTest { 15 | 16 | @Mock 17 | private lateinit var jobCommandService: JobCommandService 18 | 19 | @InjectMocks 20 | private lateinit var adminJobUseCase: AdminJobUseCase 21 | 22 | @Test 23 | fun `직업이름을 영문명, 한글명으로 받아 저장할 수 있습니다`() { 24 | // given 25 | val request = RANDOM.nextObject(SaveJobRequest::class) 26 | 27 | // when 28 | adminJobUseCase.save(request) 29 | 30 | // then 31 | then(jobCommandService).should().saveJobs(request.englishJobName, request.koreaJobName) 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/generative/interview/dto/InterviewMakerDtos.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.generative.interview.dto 2 | 3 | import app.askresume.domain.submit.constant.Satisfaction 4 | import com.querydsl.core.annotations.QueryProjection 5 | 6 | data class InterviewMakerSaveDto( 7 | val jobName: String, 8 | val difficulty: String, 9 | val careerYear: String, 10 | val language: String, 11 | val resumeType: String, 12 | val content: String, 13 | ) 14 | 15 | data class InterviewMakerPdfSaveDto( 16 | val jobName: String, 17 | val difficulty: String, 18 | val careerYear: String, 19 | val language: String, 20 | val content: String, 21 | ) 22 | 23 | data class InterviewMakerDto @QueryProjection constructor( 24 | val interviewMakerId : Long, 25 | val question : String, 26 | val bestAnswer : String, 27 | val satisfaction : Satisfaction, 28 | ) 29 | 30 | data class InterviewMakerResultDto( 31 | val question: String, 32 | val bestAnswer: String, 33 | ) 34 | 35 | data class InterviewMakerResultDtoList( 36 | val interviews : List 37 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/job/repository/JobDataRepositoryQuery.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.repository 2 | 3 | import app.askresume.domain.job.dto.JobDto 4 | import app.askresume.domain.job.dto.QJobDto 5 | import app.askresume.domain.job.model.QJob.job 6 | import app.askresume.domain.job.model.QJobMaster.jobMaster 7 | import app.askresume.domain.locale.constant.LocaleType 8 | import com.querydsl.jpa.impl.JPAQueryFactory 9 | import org.springframework.stereotype.Repository 10 | 11 | @Repository 12 | class JobDataRepositoryQuery( 13 | private val queryFactory: JPAQueryFactory, 14 | ) { 15 | 16 | fun findJobs(locale: LocaleType): List { 17 | return queryFactory 18 | .select( 19 | QJobDto( 20 | jobMaster.id, 21 | job.name, 22 | jobMaster.createdAt, 23 | jobMaster.updatedAt, 24 | ) 25 | ) 26 | .from(jobMaster) 27 | .leftJoin(job) 28 | .on(jobMaster.id.eq(job.jobMaster.id)) 29 | .where(job.locale.eq(locale)) 30 | .fetch() 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/generative/mapper/InterviewMakerMapper.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.generative.mapper 2 | 3 | import app.askresume.api.generative.vo.ResumeDataVo 4 | import app.askresume.api.generative.vo.ResumeInformationVo 5 | import app.askresume.domain.generative.interview.constant.ResumeType 6 | 7 | fun toCareer(careerYear: Int): String { 8 | return when (careerYear) { 9 | 0 -> "newcomer" 10 | 10 -> "more than 10 years" 11 | else -> "${careerYear}year" 12 | } 13 | } 14 | 15 | fun resumeDataVoListOf(contents: ResumeInformationVo): List { 16 | val mappings = mapOf( 17 | ResumeType.INTRODUCTION.value() to contents.introduction, 18 | ResumeType.CAREER.value() to contents.career, 19 | ResumeType.TECHNICAL.value() to contents.technical, 20 | ResumeType.PROJECT.value() to contents.project, 21 | ResumeType.OUTSIDE_ACTIVITIES.value() to contents.outsideActivities, 22 | ResumeType.AAC.value() to contents.aac, 23 | ) 24 | 25 | return mappings.flatMap { (resumeType, list) -> 26 | list.map { o -> ResumeDataVo(resumeType, o.content) } 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/external/openai/mapper/OpenAiMapper.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.external.openai.mapper 2 | 3 | import app.askresume.domain.submit.constant.ServiceType 4 | import app.askresume.external.openai.constant.OpenAiRole 5 | import app.askresume.external.openai.dto.ChatCompletionsMessageDto 6 | import app.askresume.external.openai.dto.ChatCompletionsRequest 7 | 8 | object OpenAiMapper { 9 | 10 | fun promptAndContentToChatCompletionsRequest( 11 | serviceType : ServiceType, 12 | prompt: String, 13 | content: String, 14 | ): ChatCompletionsRequest { 15 | 16 | return ChatCompletionsRequest( 17 | model = serviceType.model, 18 | maxTokens = serviceType.maxTokens, 19 | messages = arrayListOf( 20 | ChatCompletionsMessageDto( 21 | role = OpenAiRole.SYSTEM.value, 22 | content = prompt 23 | ), 24 | ChatCompletionsMessageDto( 25 | role = OpenAiRole.USER.value, 26 | content = content 27 | ), 28 | ) 29 | ) 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/job/service/JobReadOnlyService.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.service 2 | 3 | import app.askresume.domain.job.dto.JobDto 4 | import app.askresume.domain.job.repository.JobMasterRepository 5 | import app.askresume.domain.job.repository.findJobMasterById 6 | import app.askresume.domain.job.repository.JobDataRepositoryQuery 7 | import app.askresume.domain.locale.constant.LocaleType 8 | import org.springframework.cache.annotation.Cacheable 9 | import org.springframework.stereotype.Service 10 | import org.springframework.transaction.annotation.Transactional 11 | 12 | @Service 13 | @Transactional(readOnly = true) 14 | class JobReadOnlyService( 15 | private val jobMasterRepository: JobMasterRepository, 16 | private val jobDataRepositoryQuery : JobDataRepositoryQuery, 17 | ) { 18 | 19 | fun findJobMasterName(id: Long): String { 20 | return jobMasterRepository.findJobMasterById(id).masterName 21 | } 22 | 23 | @Cacheable(cacheNames = ["jobListCache"], key = "#locale.toString()") 24 | fun findJobs(locale: LocaleType): List { 25 | return jobDataRepositoryQuery.findJobs(locale) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/main/resources/application-staging.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: ENC(3chkk+LvuYxJqKjD6ly1f9brtSbkYazTWemsurxpnWLCkou4kzTRHQo7y+GSGEaB7JauoaEJEy6sbRtsVg4sfRy0SgJ3nlwPO7XNVs3wOK3Q8EL0lABAv8+ggCm3L9ViT4p9hW7oI3bSyxGTorgEHwxYNHB9rjvJRVPdMI7mBJ4K0gn+xUaNWQ==) 4 | username: ENC(EY2UjrJjPFCehvVShq8aOA==) 5 | password: ENC(L/1/2wgCwnq+qDZSYeM7bePmgZa3jchPZZp2kfAW9Bk=) 6 | driver-class-name: com.mysql.cj.jdbc.Driver 7 | jpa: 8 | hibernate: 9 | ddl-auto: validate 10 | properties: 11 | hibernate: 12 | format_sql: false 13 | dialect: org.hibernate.dialect.MySQL8Dialect 14 | 15 | 16 | logging: 17 | level: 18 | app.askresume: debug 19 | 20 | oauth: 21 | domain: localhost 22 | client_home: https://ask-resume-front-web-denqrtrnq-ask-resume.vercel.app 23 | providers: 24 | google: 25 | redirect-url: http://dev.ask-resume.com/api/oauth/google/callback 26 | linked-in: 27 | redirect-url: http://dev.ask-resume.com/api/oauth/linked-in/callback 28 | 29 | # Actuator Config 30 | management: 31 | endpoints: 32 | web: 33 | exposure: 34 | include: 35 | - "health" 36 | - "metrics" 37 | - "prometheus" -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/constant/OAuthProvider.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth.constant 2 | 3 | import app.askresume.oauth.exception.UnsupportedProviderException 4 | 5 | enum class OAuthProvider { 6 | GOOGLE, 7 | LINKED_IN, 8 | ; 9 | 10 | companion object { 11 | 12 | /** 13 | * 케밥 케이스로 된 String을 넣으면 Enum으로 변환한 뒤 반환합니다. 14 | * @exception UnsupportedOAuthProviderException 매칭되는 Enum 값이 없으면 예외를 던집니다. 15 | */ 16 | fun fromKebabCase(kebabCaseProvider: String): OAuthProvider { 17 | // kebab-case를 UPPER_SNAKE_CASE로 변환합니다. 18 | val upperSnakeCaseProvider = kebabCaseProvider.replace("-", "_").uppercase() 19 | 20 | // 해당 Provider가 목록에 없으면 예외를 던집니다. 21 | if (!has(upperSnakeCaseProvider)) 22 | throw UnsupportedProviderException(providerName = upperSnakeCaseProvider) 23 | 24 | // 있으면 Enum으로 변환 후 반환합니다. 25 | return OAuthProvider.valueOf(upperSnakeCaseProvider) 26 | } 27 | 28 | /** 29 | * 입력된 값이 Provider 목록에 있는지 체크합니다. 30 | */ 31 | private fun has(value: String) = OAuthProvider.values().any { it.name == value } 32 | 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/domain/locale/constant/LocaleTypeTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.locale.constant 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.DisplayName 5 | import org.junit.jupiter.api.Test 6 | 7 | class LocaleTypeTest { 8 | 9 | @Test 10 | fun `String Type의 Locale 이름을 전달하면 LocaleType enum을 반환한다`() { 11 | // given 12 | val type = "ko" 13 | 14 | // when 15 | val from = LocaleType.from(type) 16 | 17 | // then 18 | assertThat(from).isEqualTo(LocaleType.KO) 19 | } 20 | 21 | @Test 22 | fun `String Type의 Locale 이름을 전달하였을때, 해당하는 Locale이 존재하면 True를 반환한다`() { 23 | // given 24 | val type = "ko" 25 | 26 | // when 27 | val isLocaleType = LocaleType.isLocaleType(type) 28 | 29 | // then 30 | assertThat(isLocaleType).isTrue() 31 | } 32 | 33 | @Test 34 | fun `String Type의 Locale 이름을 전달하였을때, 해당하는 Locale이 존재하지 않으면 False를 반환한다`() { 35 | // given 36 | val type = "jp" 37 | 38 | // when 39 | val isLocaleType = LocaleType.isLocaleType(type) 40 | 41 | // then 42 | assertThat(isLocaleType).isFalse() 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/config/FeignConfiguration.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.config 2 | 3 | import app.askresume.global.error.FeignClientExceptionErrorDecoder 4 | import feign.Logger 5 | import feign.Retryer 6 | import org.springframework.cloud.openfeign.EnableFeignClients 7 | import org.springframework.cloud.openfeign.FeignClientsConfiguration 8 | import org.springframework.context.annotation.Bean 9 | import org.springframework.context.annotation.Configuration 10 | import org.springframework.context.annotation.Import 11 | 12 | @Configuration 13 | @EnableFeignClients(basePackages = ["app.askresume"]) 14 | @Import(FeignClientsConfiguration::class) 15 | class FeignConfiguration { 16 | 17 | @Bean 18 | fun feignLoggerLevel() = Logger.Level.FULL 19 | 20 | @Bean 21 | fun errorDecoder() = FeignClientExceptionErrorDecoder() 22 | 23 | @Bean 24 | fun retryer() = Retryer.Default( 25 | INITIAL_BACKOFF_PERIOD, 26 | MAX_BACKOFF_PERIOD, 27 | MAX_RETRY_ATTEMPTS 28 | ) 29 | 30 | companion object { 31 | const val INITIAL_BACKOFF_PERIOD: Long = 1000 * 5 32 | const val MAX_BACKOFF_PERIOD: Long = 1000 * 5 33 | const val MAX_RETRY_ATTEMPTS = 3 34 | 35 | } 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/prompt/model/Prompt.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.prompt.model 2 | 3 | import app.askresume.domain.BaseEntity 4 | import app.askresume.domain.prompt.constant.PromptType 5 | import org.hibernate.annotations.Comment 6 | import org.hibernate.annotations.SQLDelete 7 | import org.hibernate.annotations.Where 8 | import javax.persistence.* 9 | 10 | @Where(clause = "is_deleted = false") 11 | @SQLDelete(sql = "UPDATE prompt SET is_deleted = true WHERE id = ?") 12 | @Entity 13 | class Prompt( 14 | promptType: PromptType, 15 | content: String, 16 | description: String? = null, 17 | 18 | @Comment(value = "id") 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | var id: Long? = null 22 | ) : BaseEntity() { 23 | 24 | @Enumerated(EnumType.STRING) 25 | @Comment(value = "타입") 26 | @Column(length = 30, nullable = false) 27 | var promptType: PromptType = promptType 28 | protected set 29 | 30 | @Lob 31 | @Comment(value = "내용") 32 | @Column(nullable = false) 33 | var content: String = content 34 | protected set 35 | 36 | @Comment(value = "프롬프트 설명") 37 | @Column(length = 200) 38 | var description: String? = null 39 | protected set 40 | 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/generative/interview/repository/InterviewMakerQueryRepository.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.generative.interview.repository 2 | 3 | import app.askresume.domain.generative.interview.dto.InterviewMakerDto 4 | import app.askresume.domain.generative.interview.dto.QInterviewMakerDto 5 | import app.askresume.domain.generative.interview.model.QResultInterviewMaker.resultInterviewMaker 6 | import com.querydsl.jpa.impl.JPAQueryFactory 7 | import org.springframework.stereotype.Repository 8 | 9 | @Repository 10 | class InterviewMakerQueryRepository( 11 | private val queryFactory: JPAQueryFactory, 12 | ) { 13 | 14 | fun findQueryInterviewMaker(submitId: Long): List { 15 | return queryFactory 16 | .select( 17 | QInterviewMakerDto( 18 | resultInterviewMaker.id, 19 | resultInterviewMaker.question, 20 | resultInterviewMaker.bestAnswer, 21 | resultInterviewMaker.satisfaction, 22 | ) 23 | ) 24 | .from(resultInterviewMaker) 25 | .where( 26 | resultInterviewMaker.submit.id.eq(submitId) 27 | ) 28 | .fetch() 29 | } 30 | 31 | 32 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/job/model/Job.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.model 2 | 3 | import app.askresume.domain.BaseEntity 4 | import app.askresume.domain.locale.constant.LocaleType 5 | import org.hibernate.annotations.Comment 6 | import org.hibernate.annotations.SQLDelete 7 | import org.hibernate.annotations.Where 8 | import javax.persistence.* 9 | 10 | @Where(clause = "is_deleted = false") 11 | @SQLDelete(sql = "UPDATE job SET is_deleted = true WHERE id = ?") 12 | @Entity 13 | class Job( 14 | @ManyToOne(fetch = FetchType.LAZY) 15 | @JoinColumn(name = "job_master_id", foreignKey = ForeignKey(ConstraintMode.NO_CONSTRAINT)) 16 | @Comment(value = "직업마스터 ID") 17 | val jobMaster: JobMaster, 18 | 19 | name: String, 20 | locale: LocaleType, 21 | 22 | @Comment(value = "id") 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.IDENTITY) 25 | var id: Long? = null 26 | ) : BaseEntity() { 27 | 28 | @Comment(value = "직업명") 29 | @Column(length = 150, nullable = false) 30 | var name: String = name 31 | protected set 32 | 33 | @Comment(value = "언어") 34 | @Column(length = 5, nullable = false) 35 | @Enumerated(EnumType.STRING) 36 | var locale: LocaleType = locale 37 | protected set 38 | 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/job/controller/AdminJobController.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.job.controller 2 | 3 | import app.askresume.api.job.usecase.AdminJobUseCase 4 | import app.askresume.api.job.vo.SaveJobRequest 5 | import app.askresume.global.util.LoggerUtil.logger 6 | import io.swagger.v3.oas.annotations.Operation 7 | import io.swagger.v3.oas.annotations.tags.Tag 8 | import org.springframework.http.ResponseEntity 9 | import org.springframework.validation.annotation.Validated 10 | import org.springframework.web.bind.annotation.PostMapping 11 | import org.springframework.web.bind.annotation.RequestBody 12 | import org.springframework.web.bind.annotation.RequestMapping 13 | import org.springframework.web.bind.annotation.RestController 14 | import javax.validation.Valid 15 | 16 | @Tag(name = "admin job", description = "직업데이터 API (어드민)") 17 | @RestController 18 | @RequestMapping("/api/admin/jobs") 19 | class AdminJobController( 20 | private val adminJobUseCase: AdminJobUseCase, 21 | ) { 22 | 23 | val log = logger() 24 | 25 | @Tag(name = "admin job") 26 | @Operation(summary = "직업 정보를 등록하는 API", description = "직업 정보를 등록하는 API") 27 | @PostMapping 28 | fun saveJobs( 29 | @RequestBody @Valid request: SaveJobRequest, 30 | ) = adminJobUseCase.save(request) 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/error/FeignClientExceptionErrorDecode.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.error 2 | 3 | import app.askresume.global.util.LoggerUtil.logger 4 | import feign.FeignException 5 | import feign.Response 6 | import feign.RetryableException 7 | import feign.codec.ErrorDecoder 8 | import org.springframework.http.HttpStatus 9 | 10 | class FeignClientExceptionErrorDecoder( 11 | private val errorDecoder: ErrorDecoder = ErrorDecoder.Default(), 12 | ) : ErrorDecoder { 13 | 14 | private val log = logger() 15 | 16 | override fun decode(methodKey: String, response: Response): Exception { 17 | log.error("{} 요청 실패, status : {}, reason : {}", methodKey, response.status(), response.reason()) 18 | 19 | val exception = FeignException.errorStatus(methodKey, response) 20 | val httpStatus = HttpStatus.valueOf(response.status()) 21 | 22 | return if (httpStatus.is5xxServerError) { 23 | RetryableException( 24 | response.status(), 25 | exception.message, 26 | response.request().httpMethod(), 27 | exception, 28 | null, 29 | response.request() 30 | ) 31 | } else errorDecoder.decode(methodKey, response) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/mapper/SubmitDataMapper.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.mapper 2 | 3 | import app.askresume.domain.generative.interview.dto.InterviewMakerPdfSaveDto 4 | import app.askresume.domain.generative.interview.dto.InterviewMakerSaveDto 5 | import org.springframework.stereotype.Component 6 | 7 | @Component 8 | class SubmitDataMapper { 9 | 10 | fun interviewMakerSaveDtoOf(map: Map): InterviewMakerSaveDto { 11 | return InterviewMakerSaveDto( 12 | jobName = map["jobName"] as String, 13 | difficulty = map["difficulty"] as String, 14 | careerYear = map["careerYear"] as String, 15 | language = map["language"] as String, 16 | resumeType = map["resumeType"] as String, 17 | content = map["content"] as String, 18 | ) 19 | } 20 | 21 | fun interviewMakerPdfSaveDtoOf(map: Map): InterviewMakerPdfSaveDto { 22 | return InterviewMakerPdfSaveDto( 23 | jobName = map["jobName"] as String, 24 | difficulty = map["difficulty"] as String, 25 | careerYear = map["careerYear"] as String, 26 | language = map["language"] as String, 27 | content = map["content"] as String, 28 | ) 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/external/openai/dto/OpenAiDtos.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.external.openai.dto 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming 5 | 6 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) 7 | data class ChatCompletionsRequest( 8 | val model: String, 9 | val maxTokens: Int, 10 | val messages: List = mutableListOf(), 11 | ) 12 | 13 | data class ChatCompletionsMessageDto( 14 | val role: String, 15 | val content: String, 16 | ) 17 | 18 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) 19 | data class ChatCompletionsMessageResponse( 20 | val id: String, 21 | val `object`: String, 22 | val created: Long, 23 | val model: String, 24 | val choices: List = listOf(), 25 | val usage: UsageDto, 26 | ) 27 | 28 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) 29 | data class ChoicesDto( 30 | val index: Int, 31 | val message: ChatCompletionsMessageDto, 32 | val finishReason: String, 33 | ) 34 | 35 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) 36 | data class UsageDto( 37 | val promptTokens: Int, 38 | val completionTokens: Int, 39 | val totalTokens: Int, 40 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/result/model/Result.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.result.model 2 | 3 | import app.askresume.domain.BaseTimeEntity 4 | import org.hibernate.annotations.Comment 5 | import org.hibernate.annotations.Where 6 | import javax.persistence.* 7 | 8 | @Where(clause = "is_deleted = false") 9 | @Entity 10 | class Result( 11 | model: String, 12 | created: Long, 13 | promptTokens: Int, 14 | contentToken: Int, 15 | totalTokens: Int, 16 | 17 | @Comment(value = "id") 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | var id: Long? = null 21 | ) : BaseTimeEntity() { 22 | 23 | @Comment("모델") 24 | @Column(nullable = false) 25 | var model: String = model 26 | protected set 27 | 28 | @Comment("created id") 29 | @Column(nullable = false) 30 | var created: Long = created 31 | protected set 32 | 33 | @Comment("프롬프트 token") 34 | @Column(nullable = false) 35 | var promptTokens: Int = promptTokens 36 | protected set 37 | 38 | @Comment("내용 token") 39 | @Column(nullable = false) 40 | var contentToken: Int = contentToken 41 | protected set 42 | 43 | @Comment("전체 token") 44 | @Column(nullable = false) 45 | var totalTokens: Int = totalTokens 46 | protected set 47 | 48 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/domain/job/repository/JobMasterRepositoryKtTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.repository 2 | 3 | import app.askresume.fixture.JobFixture 4 | import app.askresume.RepositoryTest 5 | import app.askresume.domain.job.exception.JobNotFoundException 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.assertj.core.api.Assertions.assertThatThrownBy 8 | import org.junit.jupiter.api.DisplayName 9 | import org.junit.jupiter.api.Test 10 | import org.springframework.beans.factory.annotation.Autowired 11 | 12 | @RepositoryTest 13 | class JobMasterRepositoryKtTest { 14 | 15 | @Autowired 16 | private lateinit var jobMasterRepository: JobMasterRepository 17 | 18 | @Test 19 | fun `직업 아이디를 조회하는 경우, 직업 정보가 조회된다`() { 20 | // given 21 | val jobMaster = jobMasterRepository.save(JobFixture.jobMaster()) 22 | 23 | // when 24 | val findJobMaster = jobMasterRepository.findJobMasterById(jobMaster.id !!) 25 | 26 | // then 27 | assertThat(findJobMaster.masterName).isEqualTo(jobMaster.masterName) 28 | } 29 | 30 | @Test 31 | fun `없는 직업 아이디로 조회 하는경우, JobNotFoundException 반횐된다`() { 32 | // when & then 33 | assertThatThrownBy { jobMasterRepository.findJobMasterById(-1) } 34 | .isInstanceOf(JobNotFoundException::class.java) 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/access/usecase/AccessUseCase.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.access.usecase 2 | 3 | import app.askresume.domain.member.service.MemberCommandService 4 | import app.askresume.domain.member.service.MemberReadOnlyService 5 | import app.askresume.global.jwt.dto.JwtResponse 6 | import app.askresume.global.jwt.service.TokenManager 7 | import app.askresume.global.util.LoggerUtil.logger 8 | import org.springframework.stereotype.Service 9 | import org.springframework.transaction.annotation.Transactional 10 | 11 | @Service 12 | @Transactional(readOnly = true) 13 | class AccessUseCase( 14 | private val memberCommandService: MemberCommandService, 15 | private val memberReadOnlyService: MemberReadOnlyService, 16 | private val tokenManager: TokenManager, 17 | ) { 18 | 19 | val log = logger() 20 | 21 | fun logout(accessToken: String) { 22 | // 토큰 검증 23 | tokenManager.validateToken(accessToken) 24 | 25 | // refresh token 만료 처리 26 | val memberId = tokenManager.getMemberIdFromAccessToken(accessToken) 27 | memberCommandService.expireRefreshToken(memberId) 28 | } 29 | 30 | fun createAccessTokenByRefreshToken(refreshToken: String): JwtResponse.Token { 31 | val memberInfoDto = memberReadOnlyService.findMemberByRefreshToken(refreshToken) 32 | return tokenManager.createAccessToken(memberInfoDto.memberId, memberInfoDto.role) 33 | } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/job/service/JobCommandService.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.service 2 | 3 | import app.askresume.domain.job.model.Job 4 | import app.askresume.domain.job.model.JobMaster 5 | import app.askresume.domain.job.repository.JobMasterRepository 6 | import app.askresume.domain.job.repository.JobRepository 7 | import app.askresume.domain.locale.constant.LocaleType 8 | import app.askresume.global.util.LoggerUtil.logger 9 | import org.springframework.cache.annotation.CacheEvict 10 | import org.springframework.stereotype.Service 11 | import org.springframework.transaction.annotation.Transactional 12 | 13 | @Service 14 | @Transactional 15 | class JobCommandService( 16 | private val jobMasterRepository: JobMasterRepository, 17 | private val jobRepository: JobRepository, 18 | ) { 19 | 20 | val log = logger() 21 | 22 | @CacheEvict(cacheNames = ["jobListCache"], allEntries = true) 23 | fun saveJobs( 24 | englishJobName: String, 25 | koreaJobName: String, 26 | ) { 27 | 28 | val jobMaster = jobMasterRepository.save( 29 | JobMaster(englishJobName) 30 | ) 31 | 32 | jobRepository.save( 33 | Job(jobMaster = jobMaster, name = englishJobName, locale = LocaleType.EN) 34 | ) 35 | 36 | jobRepository.save( 37 | Job(jobMaster = jobMaster, name = koreaJobName, locale = LocaleType.KO) 38 | ) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | server: 2 | ssl: 3 | enabled: true 4 | key-store: ENC(8qtnALkqgj0QPcreREGS1PJVQPZoBUu0oayo5fJ5vAI=) 5 | key-store-password: ENC(k8aex9BuH7s5I0hoX9IXN4gT2lBgS1iQ8eI1jhuklElAbuf2W+oOtrPXcRqCliDB) 6 | key-store-type: PKCS12 7 | key-alias: tomcat 8 | 9 | spring: 10 | datasource: 11 | url: ENC(K3MI1SHMCh6yPjfQqvutuBkdjTaL5nErdT3ieJm1YbdywWgKJqD6OWzFA5gM7fbLBFll2Kg+OQVWGpzFXf+hyzbvN2PRV8V5tht5ENqBq95DtLO5se4uIcK6mjLwIVOr/u7UE4CWuwyFtetKUJ7y12FMzDWpqUsFoOuICnWszYiBs/kqmxpSaQ==) 12 | username: ENC(QG90JJ2vRU3xjEpv/HkjhBiIpwXF0Ofr) 13 | password: ENC(mdL5KV+TXIE4izGxMfrvKduAXsqmeQYmYJSzTjHQYCPTqBXc/Ojy12UlmocnPr163JglsTCGxa8=) 14 | driver-class-name: com.mysql.cj.jdbc.Driver 15 | jpa: 16 | hibernate: 17 | ddl-auto: validate 18 | properties: 19 | dialect: org.hibernate.dialect.MySQL8Dialect 20 | show-sql: false 21 | 22 | logging: 23 | level: 24 | app.askresume: info 25 | 26 | oauth: 27 | domain: ask-resume.com 28 | client-home: https://ask-resume.com 29 | providers: 30 | google: 31 | redirect-url: https://api.ask-resume.com/api/oauth/google/callback 32 | linked-in: 33 | redirect-url: https://api.ask-resume.com/api/oauth/linked-in/callback 34 | 35 | 36 | # Actuator Config 37 | management: 38 | endpoints: 39 | web: 40 | exposure: 41 | include: 42 | - "health" 43 | - "metrics" 44 | - "prometheus" -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/manager/service/PdfManagerService.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.manager.service 2 | 3 | import app.askresume.global.util.LoggerUtil.logger 4 | import org.apache.pdfbox.pdmodel.PDDocument 5 | import org.apache.pdfbox.text.PDFTextStripper 6 | import org.springframework.stereotype.Service 7 | import org.springframework.web.multipart.MultipartFile 8 | 9 | 10 | @Service 11 | class PdfManagerService { 12 | 13 | val log = logger() 14 | 15 | fun pdfToText(file: MultipartFile): String { 16 | 17 | val modifiedText = StringBuilder() 18 | 19 | file.inputStream.use { inputStream -> 20 | val document = PDDocument.load(inputStream) 21 | val pdfTextStripper = PDFTextStripper() 22 | 23 | val lines = pdfTextStripper.getText(document).lines() 24 | for (line in lines) { 25 | modifiedText.append(replaceUnicodeWithSpace(line)) 26 | modifiedText.append("\n") // 띄어쓰기 추가 27 | } 28 | } 29 | 30 | return modifiedText.toString() 31 | } 32 | 33 | private fun replaceUnicodeWithSpace(input: String): String { 34 | val output = StringBuilder() 35 | for (c in input) { 36 | if ((c in '\u0000'..'\u0020')) { 37 | output.append(' ') 38 | } else { 39 | output.append(c) 40 | } 41 | } 42 | return output.toString() 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/domain/result/service/ResultServiceTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.result.service 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.domain.result.model.Result 5 | import app.askresume.domain.result.repository.ResultRepository 6 | import org.junit.jupiter.api.DisplayName 7 | import org.junit.jupiter.api.Test 8 | import org.junit.jupiter.api.extension.ExtendWith 9 | import org.mockito.BDDMockito.* 10 | import org.mockito.InjectMocks 11 | import org.mockito.Mock 12 | import org.mockito.junit.jupiter.MockitoExtension 13 | 14 | @ExtendWith(MockitoExtension::class) 15 | class ResultServiceTest { 16 | 17 | @Mock 18 | private lateinit var resultRepository: ResultRepository 19 | 20 | @InjectMocks 21 | private lateinit var resultService: ResultService 22 | 23 | @Test 24 | fun `생성된 결과의 리소스 정보를 저장한다`() { 25 | // given 26 | val model = RANDOM.nextString() 27 | val created = RANDOM.nextLong() 28 | val promptTokens = RANDOM.nextInt() 29 | val contentToken = RANDOM.nextInt() 30 | val totalTokens = RANDOM.nextInt() 31 | 32 | val result = RANDOM.nextObject(Result::class) 33 | 34 | given(resultRepository.save(any())).willReturn(result) 35 | 36 | // when 37 | resultService.saveResultResponseData(model, created, promptTokens, contentToken, totalTokens) 38 | 39 | // then 40 | then(resultRepository).should().save(any()) 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/security/HtmlCharacterEscapes.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.security 2 | 3 | import com.fasterxml.jackson.core.SerializableString 4 | import com.fasterxml.jackson.core.io.CharacterEscapes 5 | import com.fasterxml.jackson.core.io.SerializedString 6 | import org.apache.commons.text.StringEscapeUtils 7 | 8 | class HtmlCharacterEscapes : CharacterEscapes() { 9 | 10 | private val asciiEscapes: IntArray = standardAsciiEscapesForJSON() 11 | 12 | init { 13 | // XSS 방지 처리할 특수 문자 지정 14 | asciiEscapes['<'.code] = ESCAPE_CUSTOM 15 | asciiEscapes['>'.code] = ESCAPE_CUSTOM 16 | asciiEscapes['\"'.code] = ESCAPE_CUSTOM 17 | asciiEscapes['('.code] = ESCAPE_CUSTOM 18 | asciiEscapes[')'.code] = ESCAPE_CUSTOM 19 | asciiEscapes['#'.code] = ESCAPE_CUSTOM 20 | asciiEscapes['\''.code] = ESCAPE_CUSTOM 21 | } 22 | 23 | override fun getEscapeCodesForAscii() = asciiEscapes 24 | 25 | override fun getEscapeSequence(ch: Int): SerializableString { 26 | val charAt = ch.toChar() 27 | return if (Character.isHighSurrogate(charAt) || Character.isLowSurrogate(charAt)) { 28 | val sb = StringBuilder() 29 | sb.append("\\u") 30 | sb.append(String.format("%04x", ch)) 31 | SerializedString(sb.toString()) 32 | } else { 33 | SerializedString(StringEscapeUtils.escapeHtml4(charAt.toString())) 34 | } 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/generative/factory/GenerativeCommandFactory.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.generative.factory 2 | 3 | import app.askresume.domain.generative.interview.repository.InterviewMakerRepository 4 | import app.askresume.domain.generative.interview.service.InterviewMakerCommandService 5 | import app.askresume.domain.generative.service.GenerativeCommandService 6 | import app.askresume.domain.submit.constant.ServiceType 7 | import app.askresume.domain.submit.repository.SubmitDataRepository 8 | import app.askresume.domain.submit.repository.SubmitRepository 9 | import com.fasterxml.jackson.databind.ObjectMapper 10 | import org.springframework.stereotype.Service 11 | import org.springframework.transaction.annotation.Transactional 12 | 13 | @Service 14 | @Transactional 15 | class GenerativeCommandFactory( 16 | private val objectMapper: ObjectMapper, 17 | private val submitRepository: SubmitRepository, 18 | private val submitDataRepository: SubmitDataRepository, 19 | 20 | private val interviewMakerRepository: InterviewMakerRepository 21 | ) { 22 | 23 | fun createGenerativeProvider(serviceType: ServiceType): GenerativeCommandService { 24 | return when (serviceType) { 25 | ServiceType.INTERVIEW_MAKER, ServiceType.INTERVIEW_MAKER_PDF -> InterviewMakerCommandService( 26 | objectMapper, 27 | submitRepository, 28 | submitDataRepository, 29 | interviewMakerRepository, 30 | ) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/error/ErrorResponse.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.error 2 | 3 | import org.springframework.validation.BindingResult 4 | 5 | class ErrorResponse( 6 | val errorCode: String, 7 | val errorMessage: String? = null, 8 | ) { 9 | 10 | companion object { 11 | fun of(errorCode: String, errorMessage: String?): ErrorResponse { 12 | return ErrorResponse( 13 | errorCode = errorCode, 14 | errorMessage = errorMessage, 15 | ) 16 | } 17 | 18 | fun of(errorCode: String, bindingResult: BindingResult): ErrorResponse { 19 | return ErrorResponse( 20 | errorCode = errorCode, 21 | errorMessage = createErrorMessage(bindingResult), 22 | ) 23 | } 24 | 25 | private fun createErrorMessage(bindingResult: BindingResult): String { 26 | val sb = StringBuilder() 27 | var isFirst = true 28 | 29 | val fieldErrors = bindingResult.fieldErrors 30 | for (fieldError in fieldErrors) { 31 | if (!isFirst) { 32 | sb.append(", ") 33 | } else { 34 | isFirst = false 35 | } 36 | sb.append("[") 37 | sb.append(fieldError.field) 38 | sb.append("] ") 39 | sb.append(fieldError.defaultMessage) 40 | } 41 | 42 | return sb.toString() 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/api/job/usecase/JobUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.job.usecase 2 | 3 | import app.askresume.domain.job.service.JobReadOnlyService 4 | import org.junit.jupiter.api.extension.ExtendWith 5 | import org.mockito.InjectMocks 6 | import org.mockito.Mock 7 | import org.mockito.junit.jupiter.MockitoExtension 8 | 9 | 10 | @ExtendWith(MockitoExtension::class) 11 | class JobUseCaseTest { 12 | 13 | @Mock 14 | private lateinit var jobReadOnlyService: JobReadOnlyService 15 | 16 | 17 | @InjectMocks 18 | private lateinit var jobUseCase: JobUseCase 19 | 20 | //final class라서 test 불가 21 | // @Test 22 | // fun `LocalType에 해당하는 직업 리스트를 조회하고 mapper를 통해 response 형태로 가공한다2`() { 23 | // val jobMapper: MockedStatic = mockStatic(JobMapper::class.java) 24 | // 25 | // // given 26 | // val localeType = RANDOM.nextObject(LocaleType::class) 27 | // val jobList = RANDOM.nextList(JobDto::class) 28 | // val jobResponse = JobMapper.jobResponseListOf(jobList) 29 | // 30 | // given(jobReadOnlyService.findJobs(localeType)).willReturn(jobList) 31 | // given(JobMapper.jobResponseListOf(jobList)).willReturn(jobResponse) 32 | // 33 | // // when 34 | // val findJobs = jobUseCase.findJobs(localeType) 35 | // 36 | // // then 37 | // then(jobReadOnlyService).should().findJobs(localeType) 38 | // then(JobMapper).should().jobResponseListOf(jobList) 39 | // 40 | // jobMapper.close() 41 | // } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/member/usecase/MyMemberUseCase.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.member.usecase 2 | 3 | import app.askresume.api.member.mapper.MyMemberMapper 4 | import app.askresume.api.member.vo.MemberInfoResponse 5 | import app.askresume.api.member.vo.ModifyInfoRequest 6 | import app.askresume.domain.member.service.MemberCommandService 7 | import app.askresume.domain.member.service.MemberReadOnlyService 8 | import app.askresume.global.annotation.UseCase 9 | import org.springframework.transaction.annotation.Transactional 10 | 11 | @UseCase 12 | @Transactional(readOnly = true) 13 | class MyMemberUseCase( 14 | private val memberReadOnlyService: MemberReadOnlyService, 15 | private val memberCommandService: MemberCommandService, 16 | ) { 17 | 18 | fun findMyMemberInfo(memberId: Long): MemberInfoResponse { 19 | val memberInfoDto = memberReadOnlyService.findMemberInfo(memberId) !! 20 | return MyMemberMapper.memberInfoResponseOf(memberInfoDto) 21 | } 22 | 23 | @Transactional 24 | fun modifyMyMemberInfo(memberId: Long, request: ModifyInfoRequest) { 25 | val memberInfoDto = memberReadOnlyService.findMemberInfo(memberId) !! 26 | memberCommandService.modifyMemberInfo( 27 | memberId = memberInfoDto.memberId, 28 | username = request.username, 29 | profile = request.profile, 30 | ) 31 | } 32 | 33 | @Transactional 34 | fun secessionMember(memberId: Long) { 35 | memberCommandService.secessionMember(memberId) 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/api/job/vo/SaveJobRequestTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.job.vo 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.ValidationUtils 5 | import app.askresume.fixture.SaveJobRequestFixture 6 | import org.assertj.core.api.Assertions 7 | import org.junit.jupiter.api.Test 8 | 9 | class SaveJobRequestTest { 10 | 11 | @Test 12 | fun `englishJobName와 koreaJobName는 공백일 수 없습니다`() { 13 | // given 14 | val requestA = SaveJobRequestFixture.saveJobRequest(englishJobName = " ") 15 | val requestB = SaveJobRequestFixture.saveJobRequest(koreaJobName = " ") 16 | 17 | // when 18 | val validateA = ValidationUtils.validate(requestA) 19 | val validateB = ValidationUtils.validate(requestB) 20 | 21 | // then 22 | Assertions.assertThat(validateA).isNotEmpty() 23 | Assertions.assertThat(validateB).isNotEmpty() 24 | } 25 | 26 | @Test 27 | fun `englishJobName와 koreaJobName는 150자를 넘을 수 없습니다`() { 28 | // given 29 | val requestA = SaveJobRequestFixture.saveJobRequest(englishJobName = RANDOM.nextString(minSize = 151)) 30 | val requestB = SaveJobRequestFixture.saveJobRequest(koreaJobName = RANDOM.nextString(minSize = 151)) 31 | 32 | // when 33 | val validateA = ValidationUtils.validate(requestA) 34 | val validateB = ValidationUtils.validate(requestB) 35 | 36 | // then 37 | Assertions.assertThat(validateA).isNotEmpty() 38 | Assertions.assertThat(validateB).isNotEmpty() 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/model/SubmitData.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.model 2 | 3 | import app.askresume.domain.BaseTimeEntity 4 | import app.askresume.domain.submit.constant.SubmitDataStatus 5 | import com.vladmihalcea.hibernate.type.json.JsonType 6 | import org.hibernate.annotations.Comment 7 | import org.hibernate.annotations.Type 8 | import org.hibernate.annotations.TypeDef 9 | import org.hibernate.annotations.Where 10 | import javax.persistence.* 11 | 12 | @Where(clause = "is_deleted = false") 13 | @TypeDef(name = "json", typeClass = JsonType::class) 14 | @Entity 15 | class SubmitData( 16 | parameter: Map, 17 | 18 | @ManyToOne(fetch = FetchType.LAZY) 19 | @JoinColumn(name = "submit_id", foreignKey = ForeignKey(ConstraintMode.NO_CONSTRAINT)) 20 | val submit: Submit, 21 | 22 | @Comment(value = "id") 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.IDENTITY) 25 | var id: Long? = null 26 | ) : BaseTimeEntity() { 27 | 28 | @Comment("제출한 데이터") 29 | @Type(type = "json") 30 | @Column(columnDefinition = "json") 31 | var parameter: Map = parameter 32 | protected set 33 | 34 | @Enumerated(EnumType.STRING) 35 | @Comment("상태") 36 | @Column(name = "status", nullable = false, length = 30) 37 | var submitDataStatus: SubmitDataStatus = SubmitDataStatus.WAITING 38 | protected set 39 | 40 | /** 비즈니스 로직 **/ 41 | 42 | fun updateStatus(changeStatus: SubmitDataStatus) { 43 | this.submitDataStatus = changeStatus 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /Korean.md: -------------------------------------------------------------------------------- 1 | # Ask Resume - 이력서에 대한 인터뷰 질문 및 답변 생성기 2 | 3 | 이 서비스는 이력서 인터뷰 질문과 답변을 생성하는 LLM 서비스입니다. 4 | 5 | 이 서비스에는 GPT-3.5-Turbo 모델을 사용했습니다. GPT-3.5-Turbo는 인기 있는 ChatGPT를 지원하는 OpenAI의 언어 모델입니다. 6 | 7 | 이력서를 입력하고 면접에 나올만한 질문과 그에 대한 답변을 확인해보세요! 8 | 9 | > 프론트엔드 리포지토리: https://github.com/dev-redo/ask-resume-front 10 | 11 | > 백엔드 리포지토리: https://github.com/132262B/ask-resume-backend 12 | 13 |
14 | 15 | # 어떻게 사용하나요? 16 | 17 | 1. 랜딩 페이지에서 버튼을 클릭하면 이력서를 입력할 수 있는 양식 페이지로 이동합니다. 18 | 19 | 2. 폼 페이지에서 인적사항(희망직업, 경력 등)과 이력서를 입력합니다.
20 | (참고: 언어를 변경하면 새로고침되며 입력한 값이 사라질 수 있습니다!) 21 | 22 | 3. 입력 후 제출하면 이력서에서 질문과 가능한 답변이 생성됩니다. 생성된 결과는 txt 파일로 저장할 수 있습니다. 23 | 24 |
25 | 26 | ## 사용 예시 27 | 28 | https://github.com/dev-redo/ask-resume-front/assets/69149030/49128374-77d7-4e2b-ba5a-0abc6701d5aa 29 | 30 | drawing 31 | 32 | drawing 33 | 34 |
35 | 36 | # 주의사항 37 | 38 | ## 1. 왜 결과를 생성하는데 문제가 발생하나요? 39 | 40 | 이력서를 입력하고 결과를 생성하면 서버 오류(HTTP 상태 500)가 발생할 수 있습니다. 41 | 42 | 죄송합니다, 이 오류는 GPT 서버에서 요청이 많이 들어올 때 요청을 차단하기 때문에 발생합니다.
43 | 따라서 임시방편으로 문제 발생 시 재요청이 가능하도록 구현하였습니다. 44 | 45 | 최대한 빨리 문제를 해결하도록 노력하겠습니다. 불편을 드려 정말로 죄송합니다. 46 | 47 |
48 | 49 | # 영감을 받은 리포지토리 50 | 51 | 이 서비스는 [DevPort](https://github.com/custardcream98/DevPort) 52 | 와 [gpt4-pdf-chatbot-langchain](https://github.com/mayooear/gpt4-pdf-chatbot-langchain)에서 영감을 받아 제작하였습니다. 53 | 54 |
55 | 56 | # 연락 57 | 58 | 사용 중 오류를 발견하거나 원하는 기능이 있는 경우 [Discord](https://discord.gg/aTzGNZ3y)로 문의하거나 Issue를 작성해주세요! 59 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/member/vo/MyMemberVo.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.member.vo 2 | 3 | import app.askresume.domain.member.constant.Role 4 | import io.swagger.v3.oas.annotations.media.Schema 5 | import org.hibernate.validator.constraints.URL 6 | import javax.validation.constraints.NotBlank 7 | import javax.validation.constraints.Size 8 | 9 | 10 | @Schema(name = "내 정보 수정 Request") 11 | data class ModifyInfoRequest( 12 | @field:NotBlank 13 | @field:Size(min = 2, max = 20) 14 | @field:Schema(description = "회원 이름", example = "홍길동", required = true) 15 | val username: String, 16 | 17 | @field:URL(message = "올바른 URL 형식이 아닙니다.") 18 | @field:Schema(description = "프로필 이미지 경로", example = "https://domain.com/img_110x110.jpg") 19 | val profile: String, 20 | ) 21 | 22 | @Schema(name = "회원 정보 Response") 23 | data class MemberInfoResponse( 24 | @field:Schema(description = "회원 아이디", example = "1", required = true) 25 | val memberId: Long? = null, 26 | 27 | @field:Schema(description = "이메일", example = "test@gmail.com", required = true) 28 | val email: String, 29 | 30 | @field:Schema(description = "회원 이름", example = "홍길동", required = true) 31 | val username: String, 32 | 33 | @field:Schema(description = "프로필 이미지 경로", example = "https://domain.com/img_110x110.jpg") 34 | val profile: String? = null, 35 | 36 | @field:Schema(description = "회원의 역할", example = "USER", required = true) 37 | val role: Role, 38 | 39 | @field:Schema(description = "사용자 언어", example = "EN", required = true) 40 | val locale: String, 41 | ) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/AuditingEntity.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain 2 | 3 | import org.hibernate.annotations.ColumnDefault 4 | import org.hibernate.annotations.Comment 5 | import org.hibernate.annotations.DynamicInsert 6 | import org.hibernate.annotations.DynamicUpdate 7 | import org.springframework.data.annotation.CreatedBy 8 | import org.springframework.data.annotation.LastModifiedBy 9 | import org.springframework.data.jpa.domain.support.AuditingEntityListener 10 | import java.time.LocalDateTime 11 | import javax.persistence.* 12 | 13 | @MappedSuperclass 14 | @EntityListeners(value = [AuditingEntityListener::class]) 15 | abstract class BaseEntity : BaseTimeEntity() { 16 | 17 | @Comment("등록자") 18 | @CreatedBy 19 | @Column(updatable = false) 20 | var createdBy: Long? = null 21 | 22 | @Comment("수정자") 23 | @LastModifiedBy 24 | @Column(insertable = false) 25 | var updatedBy: Long? = null 26 | } 27 | 28 | @DynamicInsert 29 | @DynamicUpdate 30 | @MappedSuperclass 31 | abstract class BaseTimeEntity { 32 | 33 | @Comment("등록일") 34 | var createdAt: LocalDateTime = LocalDateTime.now() 35 | 36 | @Comment("수정일") 37 | var updatedAt: LocalDateTime? = null 38 | 39 | @ColumnDefault("false") 40 | @Comment(value = "삭제유무") 41 | @Column(nullable = false) 42 | val isDeleted: Boolean = false 43 | 44 | @PrePersist 45 | fun prePersist() { 46 | this.createdAt = LocalDateTime.now() 47 | } 48 | 49 | @PreUpdate 50 | fun preUpdate() { 51 | this.updatedAt = LocalDateTime.now() 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/job/controller/JobController.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.job.controller 2 | 3 | import app.askresume.api.job.usecase.JobUseCase 4 | import app.askresume.api.job.vo.JobResponse 5 | import app.askresume.domain.locale.constant.LocaleType 6 | import app.askresume.domain.locale.validator.LocaleValidator 7 | import app.askresume.api.ApiResult 8 | import app.askresume.global.util.LoggerUtil.logger 9 | import io.swagger.v3.oas.annotations.Operation 10 | import io.swagger.v3.oas.annotations.tags.Tag 11 | import org.springframework.context.i18n.LocaleContextHolder 12 | import org.springframework.http.ResponseEntity 13 | import org.springframework.web.bind.annotation.GetMapping 14 | import org.springframework.web.bind.annotation.RequestMapping 15 | import org.springframework.web.bind.annotation.RestController 16 | 17 | @Tag(name = "job", description = "직업데이터 API (사용자)") 18 | @RestController 19 | @RequestMapping("/api") 20 | class JobController( 21 | private val jobUseCase: JobUseCase, 22 | private val localeValidator: LocaleValidator, 23 | ) { 24 | 25 | val log = logger() 26 | 27 | @Tag(name = "job") 28 | @Operation(summary = "언어별로 직업 리스트를 조회하는 API", description = "언어별로 직업 리스트를 조회하는 API") 29 | @GetMapping("/v1/jobs") 30 | fun findJobs(): ResponseEntity>> { 31 | var language = LocaleContextHolder.getLocale().language 32 | language = localeValidator.validateLocaleType(language) 33 | 34 | val localeType = LocaleType.from(language) 35 | 36 | return ResponseEntity.ok(ApiResult(jobUseCase.findJobs(localeType))) 37 | } 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/api/member/mapper/MyMemberMapperTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.member.mapper 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.domain.member.dto.MemberInfoDto 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.junit.jupiter.api.Test 7 | 8 | class MyMemberMapperTest { 9 | 10 | @Test 11 | fun `memberInfoDto를 MemberInfoResponse 형태로 변환한다`() { 12 | // given 13 | val memberInfoDto = RANDOM.nextObject(MemberInfoDto::class) 14 | 15 | // when 16 | val response = MyMemberMapper.memberInfoResponseOf(memberInfoDto) 17 | 18 | // then 19 | assertThat(memberInfoDto.memberId).isEqualTo(response.memberId) 20 | assertThat(memberInfoDto.email).isEqualTo(response.email) 21 | assertThat(memberInfoDto.username).isEqualTo(response.username) 22 | assertThat(memberInfoDto.profile).isEqualTo(response.profile) 23 | assertThat(memberInfoDto.role).isEqualTo(response.role) 24 | assertThat(memberInfoDto.locale).isEqualTo(response.locale) 25 | } 26 | 27 | @Test 28 | fun `memberInfoDto를 MemberInfoResponse 형태로 변환한다 (extracting)`() { 29 | // given 30 | val memberInfoDto = RANDOM.nextObject(MemberInfoDto::class) 31 | 32 | // when 33 | val response = MyMemberMapper.memberInfoResponseOf(memberInfoDto) 34 | 35 | // then 36 | assertThat(memberInfoDto) 37 | .extracting("memberId", "email", "username", "profile", "role", "locale") 38 | .containsExactly(response.memberId, response.email, response.username, response.profile, response.role, response.locale) 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/generative/interview/model/ResultInterviewMaker.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.generative.interview.model 2 | 3 | import app.askresume.domain.BaseTimeEntity 4 | import app.askresume.domain.submit.constant.Satisfaction 5 | import app.askresume.domain.submit.model.Submit 6 | import app.askresume.domain.submit.model.SubmitData 7 | import org.hibernate.annotations.Comment 8 | import org.hibernate.annotations.Where 9 | import javax.persistence.* 10 | 11 | @Where(clause = "is_deleted = false") 12 | @Entity 13 | class ResultInterviewMaker( 14 | question : String, 15 | bestAnswer : String, 16 | 17 | @ManyToOne(fetch = FetchType.LAZY) 18 | @JoinColumn(name = "submit_id", foreignKey = ForeignKey(ConstraintMode.NO_CONSTRAINT)) 19 | val submit: Submit, 20 | 21 | @ManyToOne(fetch = FetchType.LAZY) 22 | @JoinColumn(name = "submit_data_id", foreignKey = ForeignKey(ConstraintMode.NO_CONSTRAINT)) 23 | val submitData: SubmitData, 24 | 25 | @Comment(value = "id") 26 | @Id 27 | @GeneratedValue(strategy = GenerationType.IDENTITY) 28 | var id: Long? = null 29 | ) : BaseTimeEntity() { 30 | 31 | @Comment("질문") 32 | @Column(columnDefinition= "text",nullable = false) 33 | var question : String = question 34 | protected set 35 | 36 | @Comment("모범 답안") 37 | @Column(columnDefinition= "text",nullable = false) 38 | var bestAnswer : String = bestAnswer 39 | protected set 40 | 41 | @Enumerated(EnumType.STRING) 42 | @Comment("만족도") 43 | @Column(nullable = false, length = 30) 44 | var satisfaction : Satisfaction = Satisfaction.NO_RESPONSE 45 | protected set 46 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/RANDOM.kt: -------------------------------------------------------------------------------- 1 | package app.askresume 2 | 3 | import org.jeasy.random.EasyRandom 4 | import org.jeasy.random.EasyRandomParameters 5 | import kotlin.random.Random 6 | import kotlin.reflect.KClass 7 | 8 | 9 | object RANDOM { 10 | fun nextString(minSize: Int = 1, maxSize: Int = 1000): String = EasyRandomParameters() 11 | .stringLengthRange(minSize, maxSize) 12 | .seed(Random.nextLong(Long.MAX_VALUE)) 13 | .let { 14 | EasyRandom(it) 15 | }.nextObject(String::class.java) 16 | 17 | fun nextEmail(): String = 18 | "${nextString(EMAIL_MIN_LENGTH, EMAIL_MAX_LENGTH)}${TEST_DOMAIN}" 19 | 20 | fun nextInt( 21 | minSize: Int = 0, 22 | maxSize: Int = Int.MAX_VALUE 23 | ): Int = Random.nextInt(minSize, maxSize) 24 | 25 | fun nextLong( 26 | minSize: Long = 0, 27 | maxSize: Long = Long.MAX_VALUE 28 | ): Long = Random.nextLong(minSize, maxSize) 29 | 30 | fun nextObject(kClass: KClass): T { 31 | val easyRandom = EasyRandom(EasyRandomParameters().seed(Random.nextLong(Long.MAX_VALUE))) 32 | return easyRandom.nextObject(kClass.java) 33 | } 34 | 35 | fun nextList(kClass: KClass, size: Int = 10): List { 36 | val mutableList = mutableListOf() 37 | 38 | repeat(size) { 39 | val randomObject = nextObject(kClass) 40 | mutableList.add(randomObject) 41 | } 42 | return mutableList.toList() 43 | } 44 | 45 | private const val EMAIL_MIN_LENGTH = 5 46 | private const val EMAIL_MAX_LENGTH = 15 47 | 48 | private const val TEST_DOMAIN = "@mail.com" 49 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/api/job/usecase/JobUseCaseMockkTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.job.usecase 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.api.job.mapper.JobMapper 5 | import app.askresume.domain.job.dto.JobDto 6 | import app.askresume.domain.job.service.JobReadOnlyService 7 | import app.askresume.domain.locale.constant.LocaleType 8 | import io.mockk.every 9 | import io.mockk.impl.annotations.InjectMockKs 10 | import io.mockk.impl.annotations.MockK 11 | import io.mockk.junit5.MockKExtension 12 | import io.mockk.mockkObject 13 | import io.mockk.verify 14 | import org.junit.jupiter.api.Test 15 | import org.junit.jupiter.api.extension.ExtendWith 16 | 17 | 18 | @ExtendWith(MockKExtension::class) 19 | class JobUseCaseMockkTest { 20 | 21 | @MockK 22 | private lateinit var jobReadOnlyService: JobReadOnlyService 23 | 24 | @InjectMockKs 25 | private lateinit var jobUseCase: JobUseCase 26 | 27 | @Test 28 | fun `LocalType에 해당하는 직업 리스트를 조회하고 mapper를 통해 response 형태로 가공한다`() { 29 | mockkObject(JobMapper) 30 | 31 | // given 32 | val localeType = RANDOM.nextObject(LocaleType::class) 33 | val jobList = RANDOM.nextList(JobDto::class) 34 | val jobResponse = JobMapper.jobResponseListOf(jobList) 35 | 36 | every { jobReadOnlyService.findJobs(localeType) } returns jobList 37 | every { JobMapper.jobResponseListOf(jobList) } returns jobResponse 38 | 39 | // when 40 | jobUseCase.findJobs(localeType) 41 | 42 | // then 43 | verify { jobReadOnlyService.findJobs(localeType) } 44 | verify { JobMapper.jobResponseListOf(jobList) } 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/service/SubmitDataCommandService.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.service 2 | 3 | import app.askresume.domain.submit.constant.SubmitDataStatus 4 | import app.askresume.domain.submit.model.SubmitData 5 | import app.askresume.domain.submit.repository.SubmitDataRepository 6 | import app.askresume.domain.submit.repository.SubmitRepository 7 | import app.askresume.domain.submit.repository.findSubmitById 8 | import app.askresume.domain.submit.repository.findSubmitDataById 9 | import org.springframework.stereotype.Service 10 | import org.springframework.transaction.annotation.Transactional 11 | 12 | @Service 13 | @Transactional 14 | class SubmitDataCommandService( 15 | private val submitRepository: SubmitRepository, 16 | private val submitDataRepository: SubmitDataRepository 17 | ) { 18 | 19 | fun saveSubmitData( 20 | submitId: Long, 21 | parameter: Map 22 | ) { 23 | val submit = submitRepository.findSubmitById(submitId) 24 | 25 | submitDataRepository.save(SubmitData(parameter, submit)) 26 | } 27 | 28 | fun saveSubmitDataList( 29 | submitId: Long, 30 | parameters: List> 31 | ) { 32 | val submit = submitRepository.findSubmitById(submitId) 33 | 34 | parameters.map { param -> 35 | submitDataRepository.save(SubmitData(param, submit)) 36 | } 37 | } 38 | 39 | fun updateStatus( 40 | submitDataId: Long, 41 | changeStatus: SubmitDataStatus 42 | ) { 43 | val submitData = submitDataRepository.findSubmitDataById(submitDataId) 44 | submitData.updateStatus(changeStatus) 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/main/resources/message/messages_ko.properties: -------------------------------------------------------------------------------- 1 | # SYSTEM 2 | internal.server.error=서버에 문제가 발생 하였습니다. 3 | omitting.required.values=필수 매개 변수 {0}({1})가 누락되었습니다. 4 | method.not.allowed=요청 메서드 {0}이(가) 지원되지 않습니다. 5 | invalid.request.body=유효하지 않은 요청 본문입니다. 입력 내용을 확인해주세요. 6 | 7 | # MEMBER 8 | member.not.exists=존재하지 않는 맴버입니다. (memberId : {0}) 9 | already.registered.member=멤버가 이미 등록되어 있습니다. (email : {0}, member type {1}) 10 | 11 | # AUTH 12 | token.expired=토큰이 만료되었습니다. 13 | not.valid.token=토큰이 유효하지 않습니다. 14 | refresh.token.not.found=Refresh Token이 존재하지 않습니다. 15 | refresh.token.expired=Refresh Token이 만료 되었습니다. 16 | forbidden.admin=관리자 권한이 필요합니다. 17 | not.access.token.type=토큰 유형이 Access Token이 아닙니다. 18 | 19 | # FILE 20 | not.permitted.content.type=지정된 CONTENT TYPE은 허용되지 않습니다. 21 | 22 | # Difficulty 23 | difficulty.not.exists=존재하지 않는 난이도입니다. 24 | 25 | # JOB 26 | new.job.not.exists=직업이 존재하지 않습니다. (jobId : {0}) 27 | 28 | # Submit & Submit Data 29 | submit.not.exists=제출정보가 존재하지 않습니다.(submitId : {0}) 30 | submit.data.not.exists=제출 데이터가 존재하지 않습니다.(submitDataId : {0}) 31 | submit.is.not.completed=제출 데이터가 생성되지 않았습니다. (현재상태 : {0}) 32 | unauthorized.submit.access=제출 데이터에 접근할 권한이 없습니다. (submitId : {0}) 33 | content.length.over=콘텐츠 길이가 초과 되었습니다. (contentLength : {0}) 34 | content.length.lack=콘텐츠 길이가 부족합니다. (contentLength : {0}) 35 | 36 | # Prompt 37 | prompt.not.exists=Prompt가 존재하지 않습니다. (prompt type : {0}) 38 | 39 | # Cookie 40 | cookie.not.found=쿠키를 찾을 수 없습니다. (cookie name : {0}) 41 | 42 | # JWT 43 | jwt.claim.not.exists=Claim을 찾을 수 없습니다. (claim name : {0}) 44 | 45 | # OAuth 46 | oauth.provider.unsupported=지원되지 않는 OAuth Provider입니다. (provider name : {0}) 47 | oauth.userinfo.cannot.read=OAuth 유저 정보를 읽어올 수 없습니다. (provider name : {0}) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/interceptor/AdminAuthorizationInterceptor.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.interceptor 2 | 3 | import app.askresume.domain.member.constant.Role 4 | import app.askresume.global.error.exception.ForbiddenAdminException 5 | import app.askresume.global.jwt.service.TokenManager 6 | import org.springframework.beans.factory.annotation.Value 7 | import org.springframework.http.HttpHeaders 8 | import org.springframework.http.HttpMethod 9 | import org.springframework.stereotype.Component 10 | import org.springframework.web.servlet.HandlerInterceptor 11 | import javax.servlet.http.HttpServletRequest 12 | import javax.servlet.http.HttpServletResponse 13 | 14 | @Component 15 | class AdminAuthorizationInterceptor( 16 | private val tokenManager: TokenManager, 17 | @Value("\${spring.profiles.active}") var profile: String 18 | ) : HandlerInterceptor { 19 | 20 | @Throws(Exception::class) 21 | override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { 22 | if (request.method.uppercase() == HttpMethod.OPTIONS.name.uppercase()) return true 23 | if (profile.uppercase() == PROFILE_LOCAL) return true 24 | 25 | val authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION) 26 | val accessToken = authorizationHeader.split(" ")[1] 27 | 28 | val tokenClaims = tokenManager.getTokenClaims(accessToken) 29 | val role = tokenClaims["role"] as String 30 | if (Role.ADMIN != Role.valueOf(role)) { 31 | throw ForbiddenAdminException() 32 | } 33 | 34 | return true 35 | } 36 | 37 | companion object { 38 | private const val PROFILE_LOCAL = "LOCAL" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/oauth/userinfo/LinkedInUserInfo.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.oauth.userinfo 2 | 3 | import app.askresume.domain.member.constant.MemberType 4 | import app.askresume.oauth.constant.OAuthProvider 5 | import app.askresume.oauth.exception.CannotReadOAuthUserInfoException 6 | 7 | class LinkedInUserInfo( 8 | attributes: Map, 9 | ) : OAuthUserInfo(attributes) { 10 | 11 | override val email: String 12 | get() { 13 | val elements = attributes["elements"] as? List<*> 14 | val handle = elements?.get(0) as? Map<*, *> 15 | val handle2 = handle?.get("handle~") as? Map<*, *> 16 | return handle2?.get("emailAddress") as? String 17 | ?: throw CannotReadOAuthUserInfoException(OAuthProvider.LINKED_IN.name) 18 | } 19 | override val name: String 20 | get() { 21 | val firstName = attributes["localizedFirstName"] 22 | val lastName = attributes["localizedLastName"] 23 | return "$firstName $lastName" 24 | } 25 | override val profile: String? 26 | get() { 27 | val profilePicture = attributes["profilePicture"] as? Map<*, *> 28 | return profilePicture?.get("displayImage") as? String? 29 | } 30 | override val locale: String 31 | get() { 32 | val firstName = attributes["firstName"] as? Map<*, *> 33 | val preferredLocale = firstName?.get("preferredLocale") as? Map<*, *> 34 | return preferredLocale?.get("language") as? String 35 | ?: throw CannotReadOAuthUserInfoException(OAuthProvider.LINKED_IN.name) 36 | } 37 | override val memberType get() = MemberType.LINKED_IN 38 | 39 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/domain/job/service/JobCommandServiceMockkTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.service 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.domain.job.model.Job 5 | import app.askresume.domain.job.model.JobMaster 6 | import app.askresume.domain.job.repository.JobMasterRepository 7 | import app.askresume.domain.job.repository.JobRepository 8 | import io.mockk.every 9 | import io.mockk.impl.annotations.InjectMockKs 10 | import io.mockk.impl.annotations.MockK 11 | import io.mockk.junit5.MockKExtension 12 | import io.mockk.verify 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.extension.ExtendWith 15 | 16 | @ExtendWith(MockKExtension::class) 17 | class JobCommandServiceMockkTest { 18 | 19 | @MockK 20 | private lateinit var jobMasterRepository: JobMasterRepository 21 | 22 | @MockK 23 | private lateinit var jobRepository: JobRepository 24 | 25 | @InjectMockKs 26 | private lateinit var jobCommandService: JobCommandService 27 | 28 | @Test 29 | fun `직업 영문명과 한글명을 받아, 직업 정보를 언어별로 저장한다 (mockk)`() { 30 | // given 31 | val englishJobName = RANDOM.nextString(5, 30) 32 | val koreaJobName = RANDOM.nextString(5, 30) 33 | 34 | val jobMaster = RANDOM.nextObject(JobMaster::class) 35 | val job = RANDOM.nextObject(Job::class) 36 | 37 | every { jobMasterRepository.save(any()) } returns jobMaster 38 | every { jobRepository.save(any()) } returns job 39 | 40 | // when 41 | jobCommandService.saveJobs(englishJobName, koreaJobName) 42 | 43 | // then 44 | verify { jobMasterRepository.save(any()) } 45 | verify(exactly = 2) { jobRepository.save(any()) } 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/member/repository/MemberQueryRepository.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.member.repository 2 | 3 | import app.askresume.domain.member.constant.MemberType 4 | import app.askresume.domain.member.dto.MemberInfoDto 5 | import app.askresume.domain.member.dto.QMemberInfoDto 6 | import app.askresume.domain.member.model.QMember.member 7 | import app.askresume.domain.member.repository.expression.MemberExpression 8 | import com.querydsl.jpa.impl.JPAQueryFactory 9 | import org.springframework.stereotype.Repository 10 | 11 | @Repository 12 | class MemberQueryRepository( 13 | private val queryFactory: JPAQueryFactory, 14 | ) { 15 | 16 | fun findQueryMemberInfo( 17 | memberId: Long? = null, 18 | email: String? = null, 19 | memberType: MemberType? = null, 20 | refreshToken: String? = null, 21 | ): MemberInfoDto? { 22 | return queryFactory 23 | .select( 24 | QMemberInfoDto( 25 | member.id, 26 | member.email, 27 | member.memberType, 28 | member.locale, 29 | member.role, 30 | member.username, 31 | member.profile, 32 | member.refreshToken, 33 | member.tokenExpirationTime, 34 | ) 35 | ) 36 | .from(member) 37 | .where( 38 | MemberExpression.memberIdEq(memberId), 39 | MemberExpression.emailEq(email), 40 | MemberExpression.memberTypeEq(memberType), 41 | MemberExpression.refreshTokenEq(refreshToken) 42 | ) 43 | .fetchOne() 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/api/member/vo/ModifyInfoRequestTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.member.vo 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.ValidationUtils 5 | import app.askresume.fixture.ModifyInfoRequestFixture 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.junit.jupiter.api.Test 8 | 9 | class ModifyInfoRequestTest { 10 | 11 | @Test 12 | fun `프로필 주소는 URL 형태어야 합니다`() { 13 | // given 14 | val request = ModifyInfoRequestFixture.modifyInfoRequest(profile = "120x120.jpg") 15 | 16 | // when 17 | val validate = ValidationUtils.validate(request) 18 | 19 | // then 20 | assertThat(validate).isNotEmpty() 21 | } 22 | 23 | @Test 24 | fun `사용자 이름은 공백을 허용하지 않습니다`() { 25 | // given 26 | val request = ModifyInfoRequestFixture.modifyInfoRequest(username = " ") 27 | 28 | // when 29 | val validate = ValidationUtils.validate(request) 30 | 31 | // then 32 | assertThat(validate).isNotEmpty() 33 | } 34 | 35 | @Test 36 | fun `사용자 이름은 최소 2글자 이상이어야 합니다`() { 37 | // given 38 | val request = ModifyInfoRequestFixture.modifyInfoRequest(username = "j") 39 | 40 | // when 41 | val validate = ValidationUtils.validate(request) 42 | 43 | // then 44 | assertThat(validate).isNotEmpty() 45 | } 46 | 47 | @Test 48 | fun `사용자 이름은 20글자를 초과할 수 없습니다`() { 49 | // given 50 | val request = ModifyInfoRequestFixture.modifyInfoRequest(username = RANDOM.nextString(minSize = 21)) 51 | 52 | // when 53 | val validate = ValidationUtils.validate(request) 54 | 55 | // then 56 | assertThat(validate).isNotEmpty() 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/domain/job/service/JobReadOnlyServiceMockkOnlyTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.service 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.domain.job.model.JobMaster 5 | import app.askresume.domain.job.repository.JobDataRepositoryQuery 6 | import app.askresume.domain.job.repository.JobMasterRepository 7 | import app.askresume.domain.job.repository.findJobMasterById 8 | import io.mockk.every 9 | import io.mockk.impl.annotations.InjectMockKs 10 | import io.mockk.impl.annotations.MockK 11 | import io.mockk.junit5.MockKExtension 12 | import io.mockk.verify 13 | import org.assertj.core.api.Assertions.assertThat 14 | import org.junit.jupiter.api.DisplayName 15 | import org.junit.jupiter.api.Test 16 | import org.junit.jupiter.api.extension.ExtendWith 17 | 18 | @ExtendWith(MockKExtension::class) 19 | class JobReadOnlyServiceMockkOnlyTest { 20 | 21 | @MockK 22 | private lateinit var jobMasterRepository: JobMasterRepository 23 | 24 | @MockK 25 | private lateinit var jobDataRepositoryQuery: JobDataRepositoryQuery 26 | 27 | @InjectMockKs 28 | private lateinit var jobReadOnlyService: JobReadOnlyService 29 | 30 | @Test 31 | fun `직업 ID로 이용해, 직업 고유명을 조회한다`() { 32 | // given 33 | val jobMasterId = RANDOM.nextLong() 34 | val jobMaster = RANDOM.nextObject(JobMaster::class) 35 | 36 | every { jobMasterRepository.findJobMasterById(jobMasterId) } returns (jobMaster) 37 | 38 | // when 39 | val selectJobMaster = jobReadOnlyService.findJobMasterName(jobMasterId) 40 | 41 | // then 42 | verify { jobMasterRepository.findJobMasterById(jobMasterId) } 43 | assertThat(selectJobMaster).isEqualTo(jobMaster.masterName) 44 | } 45 | 46 | } 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/resolver/token/AccessTokenResolver.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.resolver.token 2 | 3 | import app.askresume.global.cookie.CookieProvider 4 | import app.askresume.global.error.exception.NotAccessTokenTypeException 5 | import app.askresume.global.jwt.constant.JwtTokenType 6 | import org.springframework.core.MethodParameter 7 | import org.springframework.stereotype.Component 8 | import org.springframework.web.bind.support.WebDataBinderFactory 9 | import org.springframework.web.context.request.NativeWebRequest 10 | import org.springframework.web.method.support.HandlerMethodArgumentResolver 11 | import org.springframework.web.method.support.ModelAndViewContainer 12 | import javax.servlet.http.HttpServletRequest 13 | 14 | @Component 15 | class AccessTokenResolver( 16 | private val cookieProvider: CookieProvider, 17 | ) : HandlerMethodArgumentResolver { 18 | 19 | override fun supportsParameter(parameter: MethodParameter): Boolean { 20 | val hasAccessTokenAnnotation = parameter.hasParameterAnnotation(AccessToken::class.java) 21 | val hasTokenDto = TokenDto::class.java.isAssignableFrom(parameter.parameterType) 22 | return hasAccessTokenAnnotation && hasTokenDto 23 | } 24 | 25 | override fun resolveArgument( 26 | parameter: MethodParameter, 27 | mavContainer: ModelAndViewContainer?, 28 | webRequest: NativeWebRequest, 29 | binderFactory: WebDataBinderFactory?, 30 | ): Any? { 31 | val request = webRequest.nativeRequest as HttpServletRequest 32 | 33 | val accessTokenCookie = cookieProvider.getCookie(request.cookies, JwtTokenType.ACCESS.cookieName) 34 | ?: throw NotAccessTokenTypeException() 35 | 36 | return TokenDto(accessTokenCookie.value) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/resolver/token/RefreshTokenResolver.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.resolver.token 2 | 3 | import app.askresume.domain.member.exception.RefreshTokenNotFoundException 4 | import app.askresume.global.cookie.CookieProvider 5 | import app.askresume.global.jwt.constant.JwtTokenType 6 | import org.springframework.core.MethodParameter 7 | import org.springframework.stereotype.Component 8 | import org.springframework.web.bind.support.WebDataBinderFactory 9 | import org.springframework.web.context.request.NativeWebRequest 10 | import org.springframework.web.method.support.HandlerMethodArgumentResolver 11 | import org.springframework.web.method.support.ModelAndViewContainer 12 | import javax.servlet.http.HttpServletRequest 13 | 14 | @Component 15 | class RefreshTokenResolver( 16 | private val cookieProvider: CookieProvider, 17 | ) : HandlerMethodArgumentResolver { 18 | override fun supportsParameter(parameter: MethodParameter): Boolean { 19 | val hasRefreshTokenAnnotation = parameter.hasParameterAnnotation(RefreshToken::class.java) 20 | val hasTokenDto = TokenDto::class.java.isAssignableFrom(parameter.parameterType) 21 | return hasRefreshTokenAnnotation && hasTokenDto 22 | } 23 | 24 | override fun resolveArgument( 25 | parameter: MethodParameter, 26 | mavContainer: ModelAndViewContainer?, 27 | webRequest: NativeWebRequest, 28 | binderFactory: WebDataBinderFactory?, 29 | ): Any? { 30 | val request = webRequest.nativeRequest as HttpServletRequest 31 | 32 | val refreshTokenCookie = cookieProvider.getCookie(request.cookies, JwtTokenType.REFRESH.cookieName) 33 | ?: throw RefreshTokenNotFoundException() 34 | 35 | return TokenDto(refreshTokenCookie.value) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/domain/submit/service/SubmitCommandServiceTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.service 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.domain.member.repository.MemberRepository 5 | import app.askresume.domain.submit.constant.SubmitStatus 6 | import app.askresume.domain.submit.model.Submit 7 | import app.askresume.domain.submit.repository.SubmitDataRepository 8 | import app.askresume.domain.submit.repository.SubmitRepository 9 | import org.junit.jupiter.api.DisplayName 10 | import org.junit.jupiter.api.Test 11 | import org.junit.jupiter.api.extension.ExtendWith 12 | import org.mockito.BDDMockito.given 13 | import org.mockito.BDDMockito.then 14 | import org.mockito.InjectMocks 15 | import org.mockito.Mock 16 | import org.mockito.junit.jupiter.MockitoExtension 17 | import java.util.* 18 | 19 | 20 | @ExtendWith(MockitoExtension::class) 21 | class SubmitCommandServiceTest { 22 | 23 | @Mock 24 | private lateinit var memberRepository: MemberRepository 25 | 26 | @Mock 27 | private lateinit var submitRepository: SubmitRepository 28 | 29 | @Mock 30 | private lateinit var submitDataRepository: SubmitDataRepository 31 | 32 | @InjectMocks 33 | private lateinit var submitCommandService: SubmitCommandService 34 | 35 | 36 | @Test 37 | fun `제출건의 상태를 변경한다`() { 38 | // given 39 | val submitId = RANDOM.nextLong() 40 | val changeStatus = RANDOM.nextObject(SubmitStatus::class) 41 | 42 | val submit = RANDOM.nextObject(Submit::class) 43 | given(submitRepository.findById(submitId)).willReturn(Optional.of(submit)) 44 | 45 | // when 46 | submitCommandService.updateStatus(submitId, changeStatus) 47 | 48 | // then 49 | then(submitRepository).should().findById(submitId) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/submit/vo/SubmitVo.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.submit.vo 2 | 3 | import app.askresume.domain.submit.constant.Satisfaction 4 | import app.askresume.domain.submit.constant.ServiceType 5 | import app.askresume.domain.submit.constant.SubmitStatus 6 | import com.fasterxml.jackson.annotation.JsonInclude 7 | import io.swagger.v3.oas.annotations.media.Schema 8 | import java.time.LocalDateTime 9 | 10 | @Schema(name = "제출 리스트 Response") 11 | data class SubmitResponse( 12 | @field:Schema(description = "제출 ID", example = "1") 13 | val submitId: Long, 14 | 15 | @field:Schema(description = "제출 제목", example = "안녕하세요. 동료와 함께 성장하는 개발자 성이름입니다.") 16 | val title: String, 17 | 18 | @field:Schema(description = "타겟 서비스", example = "INTERVIEW_MAKER") 19 | val serviceType: ServiceType, 20 | 21 | @field:Schema(description = "제출 상태", example = "WAITING") 22 | val submitStatus: SubmitStatus, 23 | 24 | @field:Schema(description = "생성일", example = "2023-06-17T15:45:20Z") 25 | val createdAt: LocalDateTime, 26 | ) 27 | 28 | @Schema(name = "제출 결과 상세 Response") 29 | @JsonInclude(JsonInclude.Include.NON_NULL) 30 | data class SubmitDetailResponse( 31 | @field:Schema(description = "타겟 서비스", example = "INTERVIEW_MAKER") 32 | val serviceType: ServiceType, 33 | @field:Schema(description = "serviceType = INTERVIEW_MAKER") 34 | val interviewMakerList: List = listOf(), 35 | ) 36 | 37 | @Schema(name = "모의인터뷰 결과 Response") 38 | data class InterviewMakerVo( 39 | @field:Schema(description = "예상 질문", example = "어쩌고...저쩌고...") 40 | val question: String, 41 | @field:Schema(description = "모범 답안", example = "어쩌고...저쩌고...") 42 | val bestAnswer: String, 43 | @field:Schema(description = "만족도", example = "NO_RESPONSE") 44 | val satisfaction : Satisfaction, 45 | ) -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/domain/job/service/JobCommandServiceTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.service 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.domain.job.model.Job 5 | import app.askresume.domain.job.model.JobMaster 6 | import app.askresume.domain.job.repository.JobMasterRepository 7 | import app.askresume.domain.job.repository.JobRepository 8 | import org.junit.jupiter.api.DisplayName 9 | import org.junit.jupiter.api.Test 10 | import org.junit.jupiter.api.extension.ExtendWith 11 | import org.mockito.ArgumentMatchers.any 12 | import org.mockito.BDDMockito.given 13 | import org.mockito.BDDMockito.then 14 | import org.mockito.InjectMocks 15 | import org.mockito.Mock 16 | import org.mockito.Mockito.times 17 | import org.mockito.junit.jupiter.MockitoExtension 18 | 19 | @ExtendWith(MockitoExtension::class) 20 | class JobCommandServiceTest { 21 | 22 | @Mock 23 | private lateinit var jobMasterRepository: JobMasterRepository 24 | 25 | @Mock 26 | private lateinit var jobRepository: JobRepository 27 | 28 | @InjectMocks 29 | private lateinit var jobCommandService: JobCommandService 30 | 31 | @Test 32 | fun `직업 영문명과 한글명을 받아, 직업 정보를 언어별로 저장한다`() { 33 | // given 34 | val englishJobName = RANDOM.nextString(5,30) 35 | val koreaJobName = RANDOM.nextString(5,30) 36 | 37 | val jobMaster = RANDOM.nextObject(JobMaster::class) 38 | val job = RANDOM.nextObject(Job::class) 39 | 40 | given(jobMasterRepository.save(any())).willReturn(jobMaster) 41 | given(jobRepository.save(any())).willReturn(job) 42 | 43 | // when 44 | jobCommandService.saveJobs(englishJobName, koreaJobName) 45 | 46 | // then 47 | then(jobMasterRepository).should().save(any()) 48 | then(jobRepository).should(times(2)).save(any()) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/submit/mapper/SubmitMapper.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.submit.mapper 2 | 3 | import app.askresume.api.submit.vo.InterviewMakerVo 4 | import app.askresume.api.submit.vo.SubmitDetailResponse 5 | import app.askresume.api.submit.vo.SubmitResponse 6 | import app.askresume.domain.generative.interview.dto.InterviewMakerDto 7 | import app.askresume.domain.submit.constant.ServiceType 8 | import app.askresume.domain.submit.dto.SubmitDto 9 | import app.askresume.api.PageResponse 10 | import org.springframework.data.domain.Page 11 | import org.springframework.data.domain.PageImpl 12 | 13 | object SubmitMapper { 14 | fun submitResponseOf(pagedSubmits: Page): PageResponse { 15 | return PageResponse( 16 | PageImpl( 17 | pagedSubmits.content.map { 18 | SubmitResponse( 19 | submitId = it.submitId, 20 | title = it.title, 21 | serviceType = it.serviceType, 22 | submitStatus = it.submitStatus, 23 | createdAt = it.createdAt, 24 | ) 25 | }, 26 | pagedSubmits.pageable, 27 | pagedSubmits.totalElements, 28 | ) 29 | ) 30 | } 31 | 32 | fun submitDetailResponseOf( 33 | serviceType: ServiceType, 34 | findInterviewMaker: List 35 | ): SubmitDetailResponse { 36 | return SubmitDetailResponse( 37 | serviceType = serviceType, 38 | interviewMakerList = findInterviewMaker.map { 39 | InterviewMakerVo( 40 | question = it.question, 41 | bestAnswer = it.bestAnswer, 42 | satisfaction = it.satisfaction, 43 | ) 44 | } 45 | ) 46 | 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/extract/controller/ExtractController.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.extract.controller 2 | 3 | import app.askresume.api.extract.usecase.ExtractUseCase 4 | import app.askresume.api.extract.vo.ExtractedTextResponse 5 | import app.askresume.domain.manager.validator.PdfManagerValidator 6 | import app.askresume.api.ApiResult 7 | import io.swagger.v3.oas.annotations.Operation 8 | import io.swagger.v3.oas.annotations.Parameter 9 | import io.swagger.v3.oas.annotations.tags.Tag 10 | import org.springframework.http.MediaType 11 | import org.springframework.http.ResponseEntity 12 | import org.springframework.validation.annotation.Validated 13 | import org.springframework.web.bind.annotation.PostMapping 14 | import org.springframework.web.bind.annotation.RequestMapping 15 | import org.springframework.web.bind.annotation.RequestPart 16 | import org.springframework.web.bind.annotation.RestController 17 | import org.springframework.web.multipart.MultipartFile 18 | 19 | @Tag(name = "extract", description = "특정 Input을 text로 추출하는 API") 20 | @Validated 21 | @RestController 22 | @RequestMapping("/api") 23 | class ExtractController( 24 | private val extractUseCase: ExtractUseCase, 25 | private val pdfManagerValidator: PdfManagerValidator, 26 | ) { 27 | 28 | @Tag(name = "extract") 29 | @Operation(summary = "이력서를 Text로 전환 API", description = "이력서를 Text로 전환 API") 30 | @PostMapping(value = ["/v1/extract/pdf"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) 31 | fun extractTextFromPdf( 32 | @Parameter(name = "resume", description = "이력서PDF파일, maxSize: 3MB, 확장자: pdf)", required = true) 33 | @RequestPart("resume") 34 | file: MultipartFile 35 | ): ResponseEntity> { 36 | pdfManagerValidator.validateContentType(file.contentType) 37 | return ResponseEntity.ok(ApiResult(extractUseCase.pdfToText(file))) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/config/jpa/AuditorAwareImpl.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.config.jpa 2 | 3 | import app.askresume.global.cookie.CookieProvider 4 | import app.askresume.global.error.exception.NotAccessTokenTypeException 5 | import app.askresume.global.jwt.constant.JwtTokenType 6 | import app.askresume.global.jwt.service.TokenManager 7 | import org.springframework.beans.factory.annotation.Autowired 8 | import org.springframework.beans.factory.annotation.Value 9 | import org.springframework.data.domain.AuditorAware 10 | import java.util.* 11 | import javax.servlet.http.HttpServletRequest 12 | 13 | class AuditorAwareImpl : AuditorAware { 14 | 15 | @Autowired 16 | private lateinit var tokenManager: TokenManager 17 | 18 | @Autowired 19 | private lateinit var httpServletRequest: HttpServletRequest 20 | 21 | @Autowired 22 | private lateinit var cookieProvider: CookieProvider 23 | 24 | @Value("\${spring.profiles.active}") 25 | private lateinit var profile: String 26 | 27 | override fun getCurrentAuditor(): Optional { 28 | if (profile.uppercase() == PROFILE_LOCAL) return Optional.of(1L) 29 | 30 | val jwtToken = extractJwtTokenFromRequest(httpServletRequest) 31 | 32 | if (jwtToken != null) { 33 | val memberId = tokenManager.getMemberIdFromAccessToken(jwtToken) 34 | return Optional.of(memberId) 35 | } else { 36 | throw RuntimeException("에러") 37 | } 38 | } 39 | 40 | private fun extractJwtTokenFromRequest(request: HttpServletRequest): String? { 41 | 42 | val accessTokenCookie = cookieProvider.getCookie(request.cookies, JwtTokenType.ACCESS.cookieName) 43 | ?: throw NotAccessTokenTypeException() 44 | return accessTokenCookie.value 45 | } 46 | 47 | companion object { 48 | private const val PROFILE_LOCAL = "LOCAL" 49 | } 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/member/service/MemberReadOnlyService.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.member.service 2 | 3 | import app.askresume.domain.member.constant.MemberType 4 | import app.askresume.domain.member.dto.MemberInfoDto 5 | import app.askresume.domain.member.exception.RefreshTokenExpiredException 6 | import app.askresume.domain.member.exception.RefreshTokenNotFoundException 7 | import app.askresume.domain.member.repository.MemberQueryRepository 8 | import org.springframework.stereotype.Service 9 | import org.springframework.transaction.annotation.Transactional 10 | import java.time.LocalDateTime 11 | 12 | @Service 13 | @Transactional(readOnly = true) 14 | class MemberReadOnlyService( 15 | private val memberQueryRepository: MemberQueryRepository, 16 | ) { 17 | 18 | fun findMemberInfo( 19 | memberId: Long? = null, 20 | email: String? = null, 21 | memberType: MemberType? = null, 22 | ): MemberInfoDto? { 23 | return memberQueryRepository.findQueryMemberInfo( 24 | memberId = memberId, 25 | email = email, 26 | memberType = memberType, 27 | ) 28 | } 29 | 30 | fun findMemberByEmail(email: String, memberType: MemberType): MemberInfoDto? { 31 | return memberQueryRepository.findQueryMemberInfo( 32 | email = email, 33 | memberType = memberType, 34 | ) 35 | } 36 | 37 | fun findMemberByRefreshToken(refreshToken: String): MemberInfoDto { 38 | val memberInfoDto = (memberQueryRepository.findQueryMemberInfo(refreshToken = refreshToken) 39 | ?: throw RefreshTokenNotFoundException()) 40 | 41 | val tokenExpirationTime = memberInfoDto.tokenExpirationTime 42 | 43 | if (tokenExpirationTime?.isBefore(LocalDateTime.now()) == true) { // DB 타입이 nullable해서 null safety 구문 추가 44 | throw RefreshTokenExpiredException() 45 | } 46 | return memberInfoDto 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/submit/usecase/SubmitUseCase.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.submit.usecase 2 | 3 | import app.askresume.api.submit.mapper.SubmitMapper 4 | import app.askresume.api.submit.vo.SubmitDetailResponse 5 | import app.askresume.api.submit.vo.SubmitResponse 6 | import app.askresume.domain.generative.interview.service.InterviewMakerReadOnlyService 7 | import app.askresume.domain.submit.constant.ServiceType 8 | import app.askresume.domain.submit.service.SubmitReadOnlyService 9 | import app.askresume.api.PageResponse 10 | import org.springframework.data.domain.Pageable 11 | import org.springframework.stereotype.Service 12 | import org.springframework.transaction.annotation.Transactional 13 | 14 | @Service 15 | @Transactional(readOnly = true) 16 | class SubmitUseCase( 17 | private val submitReadOnlyService: SubmitReadOnlyService, 18 | 19 | private val interviewMakerReadOnlyService: InterviewMakerReadOnlyService, 20 | ) { 21 | 22 | fun findMySubmits(pageable: Pageable, memberId: Long): PageResponse { 23 | val pagedSubmits = submitReadOnlyService.findSubmitList( 24 | memberId = memberId, 25 | pageable = pageable 26 | ) 27 | 28 | return SubmitMapper.submitResponseOf(pagedSubmits) 29 | } 30 | 31 | fun findMySubmitsDetail(submitId: Long, memberId: Long): SubmitDetailResponse { 32 | submitReadOnlyService.isMySubmit(submitId, memberId) 33 | submitReadOnlyService.isCompleted(submitId) 34 | 35 | return when (val serviceType = submitReadOnlyService.findSubmitServiceType(submitId)) { 36 | ServiceType.INTERVIEW_MAKER, ServiceType.INTERVIEW_MAKER_PDF -> 37 | SubmitMapper.submitDetailResponseOf( 38 | serviceType = serviceType, 39 | interviewMakerReadOnlyService.findInterviewMaker( 40 | submitId 41 | ), 42 | ) 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/model/Submit.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.model 2 | 3 | import app.askresume.domain.BaseTimeEntity 4 | import app.askresume.domain.member.model.Member 5 | import app.askresume.domain.submit.constant.ServiceType 6 | import app.askresume.domain.submit.constant.SubmitStatus 7 | import org.hibernate.annotations.Comment 8 | import org.hibernate.annotations.Where 9 | import javax.persistence.* 10 | 11 | @Where(clause = "is_deleted = false") 12 | @Entity 13 | class Submit( 14 | title: String, 15 | serviceType: ServiceType, 16 | dataCount: Int, 17 | 18 | @ManyToOne(fetch = FetchType.LAZY) 19 | @JoinColumn(name = "member_id", nullable = false, foreignKey = ForeignKey(ConstraintMode.NO_CONSTRAINT)) 20 | val member: Member, 21 | 22 | @Comment(value = "id") 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.IDENTITY) 25 | var id: Long? = null 26 | ) : BaseTimeEntity() { 27 | 28 | @Comment("제목") 29 | @Column(nullable = false, length = 50) 30 | var title: String = title 31 | protected set 32 | 33 | @Enumerated(EnumType.STRING) 34 | @Comment("서비스 타입") 35 | @Column(nullable = false, length = 40) 36 | var serviceType: ServiceType = serviceType 37 | protected set 38 | 39 | @Enumerated(EnumType.STRING) 40 | @Comment("상태") 41 | @Column(name = "status", nullable = false, length = 30) 42 | var submitStatus: SubmitStatus = SubmitStatus.WAITING 43 | protected set 44 | 45 | @Comment("시도 횟수") 46 | @Column(nullable = false) 47 | var attempts: Int = 0 48 | protected set 49 | 50 | @Comment("데이터 개수") 51 | @Column(nullable = false) 52 | var dataCount: Int = dataCount 53 | protected set 54 | 55 | /** 비즈니스 **/ 56 | 57 | fun updateStatus(changeStatus: SubmitStatus) { 58 | this.submitStatus = changeStatus 59 | } 60 | 61 | fun increaseAttempts() { 62 | this.attempts++ 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/cookie/CookieProviderImpl.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.cookie 2 | 3 | import app.askresume.global.jwt.dto.JwtResponse 4 | import org.springframework.core.env.Environment 5 | import org.springframework.http.ResponseCookie 6 | import org.springframework.stereotype.Service 7 | import java.time.Duration 8 | import javax.servlet.http.Cookie 9 | 10 | @Service 11 | class CookieProviderImpl( 12 | private val environment: Environment, 13 | ) : CookieProvider { 14 | 15 | override fun createCookie(cookieOption: CookieOption): ResponseCookie { 16 | val (key, value, httpOnly, secure, domain, path, maxAge, sameSite) = cookieOption 17 | 18 | val cookie = ResponseCookie.from(key, value) 19 | .httpOnly(httpOnly) 20 | .secure(secure) 21 | .domain(domain) 22 | .path(path) 23 | .sameSite(sameSite) 24 | 25 | maxAge?.let { cookie.maxAge(maxAge) } 26 | 27 | return cookie.build() 28 | } 29 | 30 | override fun createTokenCookie(tokenDto: JwtResponse.Token, domain: String): ResponseCookie { 31 | val isDev = environment.activeProfiles.any { profile -> profile == "dev" } 32 | val isLocal = environment.activeProfiles.any { profile -> profile == "local" } 33 | 34 | val cookieOption = CookieOption( 35 | name = tokenDto.tokenType.cookieName, 36 | value = tokenDto.token, 37 | domain = domain, 38 | maxAge = Duration.ofMillis(tokenDto.expirationTime), 39 | httpOnly = true, 40 | secure = !isLocal && !isDev, 41 | sameSite = if (isLocal || isDev) null else "None" // "local", "dev" 환경에서만 서드 파티 쿠키를 허용합니다. 42 | ) 43 | 44 | return createCookie(cookieOption) 45 | } 46 | 47 | override fun getCookie(cookies: Array?, cookieName: String): Cookie? { 48 | return cookies.let { 49 | cookies?.find { it.name == cookieName } 50 | } 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/interceptor/AuthenticationInterceptor.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.interceptor 2 | 3 | import app.askresume.global.cookie.CookieProvider 4 | import app.askresume.global.error.exception.NotAccessTokenTypeException 5 | import app.askresume.global.jwt.constant.JwtTokenType 6 | import app.askresume.global.jwt.service.TokenManager 7 | import org.springframework.beans.factory.annotation.Value 8 | import org.springframework.http.HttpMethod 9 | import org.springframework.stereotype.Component 10 | import org.springframework.web.servlet.HandlerInterceptor 11 | import javax.servlet.http.HttpServletRequest 12 | import javax.servlet.http.HttpServletResponse 13 | 14 | 15 | @Component 16 | class AuthenticationInterceptor( 17 | private val tokenManager: TokenManager, 18 | private val cookieProvider: CookieProvider, 19 | @Value("\${spring.profiles.active}") var profile: String 20 | ) : HandlerInterceptor { 21 | 22 | @Throws(Exception::class) 23 | override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { 24 | if (request.method.uppercase() == HttpMethod.OPTIONS.name.uppercase()) return true 25 | if (profile.uppercase() == PROFILE_LOCAL) return true 26 | 27 | // 1. Authorization Header 검증 28 | val accessTokenCookie = cookieProvider.getCookie(request.cookies, JwtTokenType.ACCESS.cookieName) 29 | ?: throw NotAccessTokenTypeException() 30 | 31 | // 2. 토큰 검증 32 | val accessToken = accessTokenCookie.value 33 | tokenManager.validateToken(accessToken) 34 | 35 | // 3. 토큰 타입 36 | val tokenClaims = tokenManager.getTokenClaims(accessToken) 37 | val tokenType = tokenClaims.subject 38 | if (! JwtTokenType.isAccessToken(tokenType)) { 39 | throw NotAccessTokenTypeException() 40 | } 41 | 42 | return true 43 | } 44 | 45 | companion object { 46 | private const val PROFILE_LOCAL = "LOCAL" 47 | } 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/main/resources/message/messages_en.properties: -------------------------------------------------------------------------------- 1 | # SYSTEM 2 | internal.server.error=There is a problem with the server. 3 | omitting.required.values=Required parameter {0}({1}) is missing 4 | method.not.allowed=Request method {0} not supported. 5 | invalid.request.body=Invalid request body. Please check your input. 6 | 7 | 8 | # MEMBER 9 | member.not.exists=The specified member does not exist. (memberId : {0}) 10 | already.registered.member=The member is already registered. (email : {0}, member type {1}) 11 | 12 | # AUTH 13 | token.expired=The token has expired. 14 | not.valid.token=The token is not valid. 15 | refresh.token.not.found=The specified refresh token does not exist. 16 | refresh.token.expired=The specified refresh token has expired. 17 | forbidden.admin=Administrator privileges are required. 18 | not.access.token.type=The token type is not an access token. 19 | 20 | # FILE 21 | not.permitted.content.type=The specified Content TYPE is not permitted. 22 | 23 | # Difficulty 24 | difficulty.not.exists=Difficulty level that does not exist. 25 | 26 | # JOB 27 | new.job.not.exists=The specified job does not exist. (jobId : {0}) 28 | 29 | # Submit & Submit Data 30 | submit.not.exists=Submit does not exist. (submitId : {0}) 31 | submit.data.not.exists=Submit Data does not exist.(submitDataId : {0}) 32 | submit.is.not.completed=The submission data was not generated. (Current status: {0}) 33 | unauthorized.submit.access=Unauthorized access to submission data. (submitId : {0}) 34 | content.length.over=Content length exceeded. (contentLength : {0}) 35 | content.length.lack=Content length is Insufficient. (contentLength : {0}) 36 | 37 | # Prompt 38 | prompt.not.exists=Prompt does not exist (prompt type : {0}) 39 | 40 | # Cookie 41 | cookie.not.found=Cookie does not exist. (cookie name : {0}) 42 | 43 | # JWT 44 | jwt.claim.not.exists=Claim does not exist. (claim name : {0}) 45 | 46 | # OAuth 47 | oauth.provider.unsupported=The OAuth provider is unsupported. (provider name : {0}) 48 | oauth.userinfo.cannot.read=Cannot read OAuth user info. (provider name : {0}) -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/filter/MdcLoggingFilter.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.filter 2 | 3 | import app.askresume.global.util.LoggerUtil.logger 4 | import org.slf4j.MDC 5 | import org.springframework.core.Ordered 6 | import org.springframework.core.annotation.Order 7 | import org.springframework.stereotype.Component 8 | import java.util.* 9 | import javax.servlet.Filter 10 | import javax.servlet.FilterChain 11 | import javax.servlet.ServletRequest 12 | import javax.servlet.ServletResponse 13 | import javax.servlet.http.HttpServletRequest 14 | 15 | @Component 16 | @Order(Ordered.HIGHEST_PRECEDENCE) 17 | class MdcLoggingFilter : Filter { 18 | 19 | private val log = logger() 20 | 21 | override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { 22 | val uuid = UUID.randomUUID().toString() 23 | MDC.put(TRACE_ID, uuid) 24 | 25 | val httpServletRequest = request as HttpServletRequest 26 | 27 | log.info("Incoming Request - TraceId: $uuid, IP Address: ${getClientIP(request)}, API Endpoint: [${httpServletRequest.method}]${httpServletRequest.requestURI}") 28 | chain.doFilter(request, response) 29 | MDC.clear() 30 | } 31 | 32 | fun getClientIP(request: HttpServletRequest): String { 33 | var ip = request.getHeader(X_FORWARDED_FOR) 34 | if (ip == null) ip = request.getHeader(PROXY_CLIENT_IP) 35 | if (ip == null) ip = request.getHeader(WL_PROXY_CLIENT_IP) 36 | if (ip == null) ip = request.getHeader(HTTP_CLIENT_IP) 37 | if (ip == null) ip = request.getHeader(HTTP_X_FORWARDED_FOR) 38 | if (ip == null) ip = request.remoteAddr 39 | 40 | return ip 41 | } 42 | 43 | companion object { 44 | private const val TRACE_ID = "traceId" 45 | private const val X_FORWARDED_FOR = "X-Forwarded-For" 46 | private const val PROXY_CLIENT_IP = "Proxy-Client-IP" 47 | private const val WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP" 48 | private const val HTTP_CLIENT_IP = "HTTP_CLIENT_IP" 49 | private const val HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR" 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/generative/interview/service/InterviewMakerCommandService.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.generative.interview.service 2 | 3 | import app.askresume.domain.generative.interview.dto.InterviewMakerResultDtoList 4 | import app.askresume.domain.generative.interview.model.ResultInterviewMaker 5 | import app.askresume.domain.generative.interview.repository.InterviewMakerRepository 6 | import app.askresume.domain.generative.service.GenerativeCommandService 7 | import app.askresume.domain.submit.repository.SubmitDataRepository 8 | import app.askresume.domain.submit.repository.SubmitRepository 9 | import app.askresume.domain.submit.repository.findSubmitById 10 | import app.askresume.domain.submit.repository.findSubmitDataById 11 | import app.askresume.external.openai.dto.ChoicesDto 12 | import com.fasterxml.jackson.databind.ObjectMapper 13 | import org.springframework.stereotype.Service 14 | import org.springframework.transaction.annotation.Transactional 15 | 16 | @Service 17 | @Transactional 18 | class InterviewMakerCommandService( 19 | private val objectMapper: ObjectMapper, 20 | private val submitRepository: SubmitRepository, 21 | private val submitDataRepository: SubmitDataRepository, 22 | private val interviewMakerRepository: InterviewMakerRepository, 23 | ) : GenerativeCommandService { 24 | 25 | override fun saveGenerativeResult( 26 | submitId: Long, 27 | submitDataId: Long, 28 | choices: List 29 | ) { 30 | val submit = submitRepository.findSubmitById(submitId) 31 | val submitData = submitDataRepository.findSubmitDataById(submitDataId) 32 | val result = objectMapper.readValue(choices[0].message.content, InterviewMakerResultDtoList::class.java) 33 | 34 | val resultInterviewMakerList = result.interviews.map { dto -> 35 | ResultInterviewMaker( 36 | question = dto.question, 37 | bestAnswer = dto.bestAnswer, 38 | submit = submit, 39 | submitData = submitData 40 | ) 41 | } 42 | 43 | interviewMakerRepository.saveAll(resultInterviewMakerList) 44 | } 45 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/domain/job/service/JobReadOnlyServiceMockkAndKoTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.service 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.domain.job.exception.JobNotFoundException 5 | import app.askresume.domain.job.model.JobMaster 6 | import app.askresume.domain.job.repository.JobDataRepositoryQuery 7 | import app.askresume.domain.job.repository.JobMasterRepository 8 | import app.askresume.domain.job.repository.findJobMasterById 9 | import io.kotest.core.spec.style.BehaviorSpec 10 | import io.kotest.matchers.shouldBe 11 | import io.mockk.every 12 | import io.mockk.mockk 13 | import io.mockk.verify 14 | import org.assertj.core.api.Assertions.assertThatThrownBy 15 | 16 | class JobReadOnlyServiceMockkAndKoTest : BehaviorSpec() { 17 | 18 | init { 19 | 20 | val jobMasterRepository = mockk() 21 | val jobDataRepositoryQuery = mockk() 22 | val jobReadOnlyService = JobReadOnlyService(jobMasterRepository, jobDataRepositoryQuery) 23 | 24 | Given("직업 ID를 이용해,") { 25 | val jobMasterId = RANDOM.nextLong() 26 | val jobMaster = RANDOM.nextObject(JobMaster::class) 27 | 28 | 29 | When("직업 테이블을 조회하여, 직업이 존재하면") { 30 | every { jobMasterRepository.findJobMasterById(jobMasterId) } returns jobMaster 31 | 32 | val selectJobMaster = jobReadOnlyService.findJobMasterName(jobMasterId) 33 | 34 | Then("직업 이름을 반환한다.") { 35 | verify(exactly = 1) { jobMasterRepository.findJobMasterById(jobMasterId) } 36 | selectJobMaster shouldBe jobMaster.masterName 37 | } 38 | } 39 | 40 | When("직업 테이블을 조회하여, 직업이 존재하지 않으면") { 41 | every { jobMasterRepository.findJobMasterById(- 1) }.throws(JobNotFoundException(jobMasterId)) 42 | 43 | Then("JobNotFoundException가 발생한다.") { 44 | assertThatThrownBy { jobMasterRepository.findJobMasterById(- 1) } 45 | .isInstanceOf(JobNotFoundException::class.java) 46 | } 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/member/controller/MyMemberController.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.member.controller 2 | 3 | import app.askresume.api.ApiResult 4 | import app.askresume.api.member.usecase.MyMemberUseCase 5 | import app.askresume.api.member.vo.MemberInfoResponse 6 | import app.askresume.api.member.vo.ModifyInfoRequest 7 | import app.askresume.global.resolver.memberinfo.MemberInfo 8 | import app.askresume.global.resolver.memberinfo.MemberInfoResolver 9 | import app.askresume.global.util.UriUtil 10 | import io.swagger.v3.oas.annotations.Operation 11 | import io.swagger.v3.oas.annotations.tags.Tag 12 | import org.springframework.http.HttpStatus 13 | import org.springframework.http.ResponseEntity 14 | import org.springframework.validation.annotation.Validated 15 | import org.springframework.web.bind.annotation.* 16 | import javax.validation.Valid 17 | 18 | 19 | @Tag(name = "my-member", description = "내정보 API") 20 | @RestController 21 | @RequestMapping("/api/my-member") 22 | class MyMemberController( 23 | private val myMemberUseCase: MyMemberUseCase, 24 | ) { 25 | 26 | @Tag(name = "my-member") 27 | @Operation(summary = "내 정보 조회 API", description = "내 정보 조회 API") 28 | @GetMapping 29 | fun findMyInfo( 30 | @MemberInfoResolver memberInfo: MemberInfo 31 | ): ApiResult = ApiResult(myMemberUseCase.findMyMemberInfo(memberInfo.memberId)) 32 | 33 | @Tag(name = "my-member") 34 | @Operation(summary = "내 정보 변경 API", description = "내 정보 변경 API") 35 | @PutMapping 36 | fun modify( 37 | @Valid @RequestBody request: ModifyInfoRequest, 38 | @MemberInfoResolver memberInfo: MemberInfo 39 | ): ResponseEntity { 40 | myMemberUseCase.modifyMyMemberInfo(memberInfo.memberId, request) 41 | 42 | return ResponseEntity.created(UriUtil.createUri()).build() 43 | } 44 | 45 | @ResponseStatus(HttpStatus.NO_CONTENT) 46 | @Tag(name = "my-member") 47 | @Operation(summary = "회원 탈퇴 API", description = "회원 탈퇴 API") 48 | @DeleteMapping 49 | fun secession( 50 | @MemberInfoResolver memberInfo: MemberInfo 51 | ) = myMemberUseCase.secessionMember(memberInfo.memberId) 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/main/resources/lucy-xss-superset-sax.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/error/ErrorCodes.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.error 2 | 3 | import org.springframework.http.HttpStatus 4 | 5 | enum class ErrorCodes( 6 | val errorCode: String, 7 | val description: String 8 | ) { 9 | 10 | INTERNAL_SERVER_ERROR("SYS-001", "서버 내부 오류가 발생했습니다."), 11 | OMITTING_REQUIRED_VALUES("SYS-002", "필수값이 누락되었습니다."), 12 | METHOD_NOT_ALLOWED("SYS-003", "지원하지 않는 요청 메서드입니다."), 13 | INVALID_REQUEST_BODY("SYS-004", "유효하지 않은 요청 본문입니다."), 14 | 15 | ENUM_VALIDATE_NOT_EXIST("VAL-001", "enum validate check fail"), 16 | 17 | ALREADY_REGISTERED_MEMBER("MEM-001", "이미 등록된 회원입니다."), 18 | 19 | ENTITY_NOT_FOUND("ENY-001", "Entity 데이터가 존재하지 않습니다."), 20 | 21 | INVALID_CONTENT("CON-001","유효하지 않는 컨텐츠(사이즈 등)"), 22 | 23 | STATUS_IS_NOT_COMPLETED("SNC-001", "COMPLETED 되지 않은 내용에 접근 시도"), 24 | 25 | TOKEN_EXPIRED("AUTH-001", "토큰이 만료 되었습니다."), 26 | NOT_VALID_TOKEN("AUTH-002", "토큰이 유효하지 않습니다."), 27 | REFRESH_TOKEN_NOT_FOUND("AUTH-005", "Refresh Token이 존재하지 않습니다."), 28 | REFRESH_TOKEN_EXPIRED("AUTH-006", "Refresh Token이 만료 되었습니다."), 29 | NOT_ACCESS_TOKEN_TYPE("AUTH-007", "Access Token이 아닙니다."), 30 | FORBIDDEN_ADMIN("AUTH-008", "관리자권한이 필요합니다."), 31 | UNAUTHORIZED_ACCESS("AUTH-009", "접근 권한이 없습니다."), 32 | 33 | NOT_PERMITTED_CONTENT_TYPE("FILE-001", "허가되지 않는 CONTENT TYPE 입니다."), 34 | ; 35 | 36 | fun toHttpStatus(): HttpStatus = when (this) { 37 | INTERNAL_SERVER_ERROR -> HttpStatus.INTERNAL_SERVER_ERROR 38 | 39 | // 400 40 | INVALID_CONTENT, 41 | OMITTING_REQUIRED_VALUES, 42 | ALREADY_REGISTERED_MEMBER, 43 | ENUM_VALIDATE_NOT_EXIST, 44 | ENTITY_NOT_FOUND, 45 | STATUS_IS_NOT_COMPLETED, 46 | INVALID_REQUEST_BODY, 47 | NOT_PERMITTED_CONTENT_TYPE -> HttpStatus.BAD_REQUEST 48 | 49 | // 401 50 | REFRESH_TOKEN_NOT_FOUND, 51 | REFRESH_TOKEN_EXPIRED, 52 | TOKEN_EXPIRED, 53 | NOT_ACCESS_TOKEN_TYPE, 54 | NOT_VALID_TOKEN -> HttpStatus.UNAUTHORIZED 55 | 56 | // 403 57 | UNAUTHORIZED_ACCESS, 58 | FORBIDDEN_ADMIN -> HttpStatus.FORBIDDEN 59 | 60 | // 405 61 | METHOD_NOT_ALLOWED -> HttpStatus.METHOD_NOT_ALLOWED 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /src/test/kotlin/app/askresume/domain/job/service/JobReadOnlyServiceTest.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.job.service 2 | 3 | import app.askresume.RANDOM 4 | import app.askresume.domain.job.dto.JobDto 5 | import app.askresume.domain.job.model.JobMaster 6 | import app.askresume.domain.job.repository.JobDataRepositoryQuery 7 | import app.askresume.domain.job.repository.JobMasterRepository 8 | import app.askresume.domain.locale.constant.LocaleType 9 | import org.assertj.core.api.Assertions.assertThat 10 | import org.junit.jupiter.api.Test 11 | import org.junit.jupiter.api.extension.ExtendWith 12 | import org.mockito.BDDMockito.given 13 | import org.mockito.BDDMockito.then 14 | import org.mockito.InjectMocks 15 | import org.mockito.Mock 16 | import org.mockito.junit.jupiter.MockitoExtension 17 | import java.util.* 18 | 19 | @ExtendWith(MockitoExtension::class) 20 | class JobReadOnlyServiceTest { 21 | 22 | @Mock 23 | private lateinit var jobMasterRepository: JobMasterRepository 24 | 25 | @Mock 26 | private lateinit var jobDataRepositoryQuery : JobDataRepositoryQuery 27 | 28 | @InjectMocks 29 | private lateinit var jobReadOnlyService : JobReadOnlyService 30 | 31 | @Test 32 | fun `직업 ID로 이용해, 직업 고유명을 조회한다`() { 33 | // given 34 | val jobMasterId = RANDOM.nextLong() 35 | 36 | val jobMaster = RANDOM.nextObject(JobMaster::class) 37 | 38 | given(jobMasterRepository.findById(jobMasterId)).willReturn(Optional.of(jobMaster)) 39 | 40 | // when 41 | val selectJobMaster = jobReadOnlyService.findJobMasterName(jobMasterId) 42 | 43 | // then 44 | then(jobMasterRepository).should().findById(jobMasterId) 45 | assertThat(selectJobMaster).isEqualTo(jobMaster.masterName) 46 | } 47 | 48 | 49 | @Test 50 | fun `locale 정보로 해당 언어의 직업을 조회한다`() { 51 | // given 52 | val localeType = RANDOM.nextObject(LocaleType::class) 53 | 54 | val list = RANDOM.nextList(JobDto::class) 55 | given(jobDataRepositoryQuery.findJobs(localeType)).willReturn(list) 56 | 57 | // when 58 | jobReadOnlyService.findJobs(localeType) 59 | 60 | // then 61 | then(jobDataRepositoryQuery).should().findJobs(localeType) 62 | } 63 | 64 | } 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ask Resume - Interview questions and answers generator for your resume 2 | 3 | [한국어 Readme](https://github.com/dev-redo/ask-resume-front/blob/main/Korean.md) 4 | 5 | This service is an LLM service to generate interview questions and answers for your resume. 6 | 7 | We used the GPT-3.5-Turbo model for this service. GPT-3.5-Turbo is OpenAI's language model which powers the popular ChatGPT. 8 | 9 | Enter your resume information and check out the Q&A that will come up in the interview! 10 | 11 | > Frontend repository: https://github.com/dev-redo/ask-resume-front 12 | 13 | > Backend repository: https://github.com/132262B/ask-resume-backend 14 | 15 |
16 | 17 | # How to use? 18 | 19 | 1. Clicking the button on the landing page will take you to the form page where you can enter your resume. 20 | 21 | 2. On the form page, enter your personal information (desired occupation, experience, etc.) and your resume.
22 | (Note: If you change the language, it will be refreshed and the values you entered may be lost!) 23 | 24 | 3. Submitting after inputting will generate questions and possible answers from your resume. The generated results can be saved as a txt file. 25 | 26 |
27 | 28 | ## example of use 29 | 30 | https://github.com/dev-redo/ask-resume-front/assets/69149030/8a24369c-a5e7-44a5-9114-eb5a520a99c4 31 | 32 | drawing 33 | 34 | drawing 35 | 36 | 37 |
38 | 39 | # Caption 40 | 41 | ## 1. Why is it failing to produce results? 42 | 43 | If you enter your resume and generate results, a server error (HTTP status 500) may occur. 44 | 45 | Sorry bro, This error occurs because the GPT server blocks requests when a large number of requests come in.
46 | Therefore, it has been implemented so that a re-request can be made when the issue occurs. 47 | 48 | We will try to resolve the issue as soon as possible. Really sorry for the inconvenience. 49 | 50 |
51 | 52 | # Credit 53 | 54 | This service is inspired by [DevPort](https://github.com/custardcream98/DevPort) and [gpt4-pdf-chatbot-langchain](https://github.com/mayooear/gpt4-pdf-chatbot-langchain) 55 | 56 |
57 | 58 | # Contact 59 | 60 | If you find an error while using it or have a desired function, Please contact me by [Discord](https://discord.gg/aTzGNZ3y) or write Issue! 61 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/submit/controller/SubmitController.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.submit.controller 2 | 3 | import app.askresume.api.submit.usecase.SubmitUseCase 4 | import app.askresume.api.submit.vo.SubmitDetailResponse 5 | import app.askresume.api.submit.vo.SubmitResponse 6 | import app.askresume.api.ApiResult 7 | import app.askresume.api.PageResponse 8 | import app.askresume.global.resolver.memberinfo.MemberInfo 9 | import app.askresume.global.resolver.memberinfo.MemberInfoResolver 10 | import io.swagger.v3.oas.annotations.Operation 11 | import io.swagger.v3.oas.annotations.tags.Tag 12 | import org.springdoc.api.annotations.ParameterObject 13 | import org.springframework.data.domain.Pageable 14 | import org.springframework.web.bind.annotation.GetMapping 15 | import org.springframework.web.bind.annotation.PathVariable 16 | import org.springframework.web.bind.annotation.RequestMapping 17 | import org.springframework.web.bind.annotation.RestController 18 | 19 | @Tag(name = "submit", description = "제출 정보 API") 20 | @RestController 21 | @RequestMapping("/api/submits") 22 | class SubmitController( 23 | private val useCase: SubmitUseCase, 24 | ) { 25 | 26 | @Tag(name = "submit") 27 | @Operation( 28 | summary = "내 제출 정보 목록 조회 API", 29 | description = """ 30 | 이 API는 사용자가 제출한 생성형 AI 서비스 요청 목록을 페이징 처리하여 조회하는 데 사용됩니다. 31 | 32 | 유저는 페이지 번호와 페이지당 결과 수를 지정하여 원하는 범위의 데이터를 가져올 수 있습니다. 33 | """ 34 | ) 35 | @GetMapping 36 | fun findMySubmits( 37 | @ParameterObject pageable: Pageable, 38 | @MemberInfoResolver memberInfo: MemberInfo, 39 | ): ApiResult> = ApiResult(useCase.findMySubmits(pageable, memberInfo.memberId)) 40 | 41 | @Tag(name = "submit") 42 | @Operation( 43 | summary = "생성형 AI 서비스 생성 결과 상세조회", 44 | description = """ 45 | 이 API는 생성형 AI 서비스의 결과를 상세 조회하는 데 사용됩니다. 46 | 사용자는 서비스 식별자(submitId)를 입력하여 해당 서비스가 생성한 데이터를 가져올 수 있습니다. 47 | serviceType에 따라, 다른 response을 전달합니다. 48 | """ 49 | ) 50 | @GetMapping("/{submitId}") 51 | fun findMySubmitDetail( 52 | @PathVariable submitId: Long, 53 | @MemberInfoResolver memberInfo: MemberInfo, 54 | ): ApiResult = ApiResult(useCase.findMySubmitsDetail(submitId, memberInfo.memberId)) 55 | 56 | } -------------------------------------------------------------------------------- /src/main/resources/templates/error/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ask-resume error page 6 | 7 | 83 | 84 |
404
85 | 86 | -------------------------------------------------------------------------------- /src/main/resources/templates/error/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ask-resume error page 6 | 7 | 83 | 84 |
500
85 | 86 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/submit/service/SubmitReadOnlyService.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.submit.service 2 | 3 | import app.askresume.domain.member.repository.MemberRepository 4 | import app.askresume.domain.member.repository.findMemberById 5 | import app.askresume.domain.submit.constant.ServiceType 6 | import app.askresume.domain.submit.constant.SubmitStatus 7 | import app.askresume.domain.submit.dto.FirstSubmittedDto 8 | import app.askresume.domain.submit.dto.SubmitDto 9 | import app.askresume.domain.submit.exception.SubmitStatusIsNotCompletedException 10 | import app.askresume.domain.submit.repository.SubmitQueryRepository 11 | import app.askresume.domain.submit.repository.SubmitRepository 12 | import app.askresume.domain.submit.repository.existsSubmitByIdAndMember 13 | import app.askresume.domain.submit.repository.findSubmitById 14 | import org.springframework.data.domain.Page 15 | import org.springframework.data.domain.Pageable 16 | import org.springframework.stereotype.Service 17 | import org.springframework.transaction.annotation.Transactional 18 | 19 | @Service 20 | @Transactional(readOnly = true) 21 | class SubmitReadOnlyService( 22 | private val submitRepository: SubmitRepository, 23 | private val memberRepository: MemberRepository, 24 | 25 | private val submitQueryRepository: SubmitQueryRepository, 26 | ) { 27 | fun findRequestedFirstSubmit(): FirstSubmittedDto? { 28 | return submitQueryRepository.findQueryRequestedFirstSubmit() 29 | } 30 | 31 | fun findSubmitList( 32 | memberId: Long? = null, 33 | pageable: Pageable 34 | ): Page { 35 | return submitQueryRepository.findQuerySubmitList( 36 | memberId = memberId, 37 | pageable = pageable, 38 | ) 39 | } 40 | 41 | fun isMySubmit(submitId: Long, memberId: Long) { 42 | val member = memberRepository.findMemberById(memberId) 43 | submitRepository.existsSubmitByIdAndMember(submitId, member) 44 | } 45 | 46 | fun findSubmitServiceType(submitId: Long) : ServiceType { 47 | return submitRepository.findSubmitById(submitId).serviceType 48 | } 49 | 50 | fun isCompleted(submitId: Long) { 51 | val status = submitRepository.findSubmitById(submitId).submitStatus 52 | 53 | if(status!= SubmitStatus.COMPLETED) 54 | throw SubmitStatusIsNotCompletedException(status) 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/member/service/MemberCommandService.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.member.service 2 | 3 | import app.askresume.domain.member.constant.MemberType 4 | import app.askresume.domain.member.constant.Role 5 | import app.askresume.domain.member.model.Member 6 | import app.askresume.domain.member.repository.MemberRepository 7 | import app.askresume.domain.member.repository.findMemberById 8 | import app.askresume.domain.member.repository.validateDuplicateMember 9 | import org.springframework.stereotype.Service 10 | import org.springframework.transaction.annotation.Transactional 11 | import java.time.LocalDateTime 12 | import java.util.* 13 | 14 | @Service 15 | @Transactional 16 | class MemberCommandService( 17 | private val memberRepository: MemberRepository, 18 | ) { 19 | 20 | fun registerMember( 21 | email: String, 22 | name: String, 23 | profile: String?, 24 | locale: String, 25 | memberType: MemberType, 26 | ): Long { 27 | memberRepository.validateDuplicateMember(email, memberType) 28 | 29 | return memberRepository.save( 30 | Member( 31 | email = email, 32 | memberType = memberType, 33 | locale = locale, 34 | role = Role.USER, 35 | username = name, 36 | profile = profile, 37 | ) 38 | ).id !! 39 | } 40 | 41 | 42 | fun secessionMember(memberId: Long) { 43 | val member = memberRepository.findMemberById(memberId) 44 | 45 | memberRepository.delete(member) 46 | } 47 | 48 | fun expireRefreshToken(memberId: Long) { 49 | val member = memberRepository.findMemberById(memberId) 50 | 51 | member.expireRefreshToken(LocalDateTime.now()) 52 | } 53 | 54 | fun modifyMemberInfo(memberId: Long, username: String, profile: String) { 55 | val member = memberRepository.findMemberById(memberId) 56 | 57 | member.changeMemberInfo( 58 | username = username, 59 | profile = profile, 60 | ) 61 | } 62 | 63 | fun updateRefreshToken(memberId: Long, refreshToken: String, expireDate: Date) { 64 | val member = memberRepository.findMemberById(memberId) 65 | 66 | member.updateRefreshToken( 67 | refreshToken = refreshToken, 68 | expireDate = expireDate, 69 | ) 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/config/OpenApiConfig.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.config 2 | 3 | import io.swagger.v3.oas.models.Components 4 | import io.swagger.v3.oas.models.OpenAPI 5 | import io.swagger.v3.oas.models.info.Info 6 | import io.swagger.v3.oas.models.security.SecurityRequirement 7 | import io.swagger.v3.oas.models.security.SecurityScheme 8 | import io.swagger.v3.oas.models.servers.Server 9 | import org.springframework.context.annotation.Bean 10 | import org.springframework.context.annotation.Configuration 11 | import org.springframework.http.HttpHeaders 12 | 13 | @Configuration 14 | class OpenApiConfig { 15 | 16 | @Bean 17 | fun openAPI(): OpenAPI { 18 | return OpenAPI() 19 | .components( 20 | Components() 21 | .addSecuritySchemes(JWT_TOKEN, getSecurityScheme()[0]) 22 | ) 23 | .info(getInfo()) 24 | .servers(getServers()) 25 | .addSecurityItem(getSecurityRequirements()[0]) 26 | } 27 | 28 | // swagger 설명 info 작업 29 | private fun getInfo(): Info { 30 | return Info() 31 | .title(SWAGGER_TITLE) 32 | .description(SWAGGER_DESCRIPTION) 33 | .version(API_VERSION) 34 | // .termsOfService("http://swagger.io/terms/") 35 | // .contact(Contact().name("Igor").url("http://132262b.github.io").email("132262b@naver.com")) 36 | // .license(License().name("Apache License Version 2.0").url("http://www.apache.org/licenses/LICENSE-2.0")); 37 | } 38 | 39 | private fun getSecurityScheme(): List { 40 | return listOf( 41 | SecurityScheme() 42 | .type(SecurityScheme.Type.APIKEY) 43 | .name(HttpHeaders.AUTHORIZATION) // 헤더 필드 이름 (이름을 변경하여 사용 가능) 44 | .`in`(SecurityScheme.In.HEADER) 45 | ) 46 | } 47 | 48 | private fun getSecurityRequirements() = listOf(SecurityRequirement().addList(JWT_TOKEN)) 49 | 50 | // swagger에 server 종류 추가 51 | private fun getServers(): List { 52 | return listOf( 53 | Server().url("http://localhost:8080").description("localhost"), 54 | Server().url("http://dev.ask-resume.com").description("develop") 55 | ) 56 | } 57 | 58 | companion object { 59 | const val SWAGGER_TITLE = "ask-resume API DOCS " 60 | const val SWAGGER_DESCRIPTION = "" 61 | const val API_VERSION = "1.2.0" 62 | 63 | const val JWT_TOKEN = "jwt token" 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/generative/controller/InterviewMakerController.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.generative.controller 2 | 3 | import app.askresume.api.generative.usecase.InterviewMakerUseCase 4 | import app.askresume.api.generative.vo.InformationRequest 5 | import app.askresume.api.generative.vo.InterviewMakerRequest 6 | import app.askresume.domain.manager.validator.PdfManagerValidator 7 | import app.askresume.global.resolver.memberinfo.MemberInfo 8 | import app.askresume.global.resolver.memberinfo.MemberInfoResolver 9 | import io.swagger.v3.oas.annotations.Operation 10 | import io.swagger.v3.oas.annotations.Parameter 11 | import io.swagger.v3.oas.annotations.tags.Tag 12 | import org.springframework.http.HttpStatus 13 | import org.springframework.http.MediaType 14 | import org.springframework.http.ResponseEntity 15 | import org.springframework.validation.annotation.Validated 16 | import org.springframework.web.bind.annotation.* 17 | import org.springframework.web.multipart.MultipartFile 18 | import javax.validation.Valid 19 | 20 | @Tag(name = "generative", description = "생성, 스케줄 대기열에 등록 API") 21 | @RestController 22 | @RequestMapping("/api/generative/interview-maker") 23 | class InterviewMakerController( 24 | private val interviewMakerUseCase: InterviewMakerUseCase, 25 | private val pdfManagerValidator: PdfManagerValidator, 26 | ) { 27 | 28 | @ResponseStatus(HttpStatus.CREATED) 29 | @Tag(name = "generative") 30 | @Operation(summary = "[interview-maker] 수기 입력 제출, 예상 질문, 모범 답안 대기열에 등록 API") 31 | @PostMapping("/manual") 32 | fun saveManualSubmit( 33 | @Valid @RequestBody request: InterviewMakerRequest, 34 | @MemberInfoResolver memberInfo: MemberInfo, 35 | ) = interviewMakerUseCase.saveManualSubmit(request, memberInfo) 36 | 37 | @Tag(name = "generative") 38 | @Operation(summary = "[interview-maker] PDF 제출, 예상 질문, 모범 답안 대기열에 등록 API",) 39 | @PostMapping(value = ["/pdf"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) 40 | fun savePdfSubmit( 41 | @Valid request: InformationRequest, 42 | @Parameter(name = "resume", description = "이력서PDF파일, maxSize: 3MB, 확장자: pdf)", required = true) 43 | @RequestPart("resume") file: MultipartFile, 44 | @MemberInfoResolver memberInfo: MemberInfo, 45 | ): ResponseEntity { 46 | pdfManagerValidator.validateContentType(file.contentType) 47 | interviewMakerUseCase.savePdfSubmit(request, file, memberInfo) 48 | 49 | return ResponseEntity.status(HttpStatus.CREATED).build() 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/domain/prompt/service/PromptReadOnlyService.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.domain.prompt.service 2 | 3 | import app.askresume.domain.prompt.constant.PromptType 4 | import app.askresume.domain.prompt.repository.PromptRepository 5 | import app.askresume.domain.prompt.repository.findPromptByPromptType 6 | import app.askresume.domain.submit.constant.ServiceType 7 | import app.askresume.domain.submit.mapper.SubmitDataMapper 8 | import app.askresume.global.util.LoggerUtil.logger 9 | import org.springframework.cache.annotation.Cacheable 10 | import org.springframework.stereotype.Service 11 | import org.springframework.transaction.annotation.Transactional 12 | 13 | 14 | @Service 15 | @Transactional(readOnly = true) 16 | class PromptReadOnlyService( 17 | private val promptRepository: PromptRepository, 18 | 19 | private val submitDataMapper: SubmitDataMapper, 20 | ) { 21 | 22 | val log = logger() 23 | 24 | @Cacheable(cacheNames = ["promptCache"], key = "#promptType.toString()") 25 | fun findByPromptType(promptType: PromptType): String { 26 | return promptRepository.findPromptByPromptType(promptType).content 27 | } 28 | 29 | fun findPromptAndFormatting( 30 | serviceType: ServiceType, 31 | parameter: Map 32 | ): String { 33 | 34 | return when (serviceType) { 35 | ServiceType.INTERVIEW_MAKER -> { 36 | val prompt = findByPromptType(PromptType.INTERVIEW_MAKER) 37 | val interviewMakerDto = submitDataMapper.interviewMakerSaveDtoOf(parameter) 38 | 39 | String.format( 40 | prompt, 41 | interviewMakerDto.jobName, 42 | interviewMakerDto.resumeType, 43 | interviewMakerDto.difficulty, 44 | interviewMakerDto.careerYear, 45 | interviewMakerDto.language 46 | ) 47 | } 48 | ServiceType.INTERVIEW_MAKER_PDF -> { 49 | val prompt = findByPromptType(PromptType.INTERVIEW_MAKER_PDF) 50 | val interviewMakerPdfSaveDto = submitDataMapper.interviewMakerPdfSaveDtoOf(parameter) 51 | 52 | String.format( 53 | prompt, 54 | interviewMakerPdfSaveDto.jobName, 55 | interviewMakerPdfSaveDto.difficulty, 56 | interviewMakerPdfSaveDto.careerYear, 57 | interviewMakerPdfSaveDto.language 58 | ) 59 | } 60 | } 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/global/resolver/memberinfo/MemberInfoArgumentResolver.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.global.resolver.memberinfo 2 | 3 | import app.askresume.domain.member.constant.Role 4 | import app.askresume.global.cookie.CookieProvider 5 | import app.askresume.global.error.exception.NotAccessTokenTypeException 6 | import app.askresume.global.jwt.constant.JwtTokenType 7 | import app.askresume.global.jwt.service.TokenManager 8 | import org.springframework.beans.factory.annotation.Value 9 | import org.springframework.core.MethodParameter 10 | import org.springframework.stereotype.Component 11 | import org.springframework.web.bind.support.WebDataBinderFactory 12 | import org.springframework.web.context.request.NativeWebRequest 13 | import org.springframework.web.method.support.HandlerMethodArgumentResolver 14 | import org.springframework.web.method.support.ModelAndViewContainer 15 | import javax.servlet.http.HttpServletRequest 16 | 17 | @Component 18 | class MemberInfoArgumentResolver( 19 | private val tokenManager: TokenManager, 20 | private val cookieProvider: CookieProvider, 21 | @Value("\${spring.profiles.active}") var profile: String 22 | ) : HandlerMethodArgumentResolver { 23 | 24 | override fun supportsParameter(parameter: MethodParameter): Boolean { 25 | val hasMemberInfoAnnotation = parameter.hasParameterAnnotation(MemberInfoResolver::class.java) 26 | val hasMemberInfo = MemberInfo::class.java.isAssignableFrom(parameter.parameterType) 27 | return hasMemberInfoAnnotation && hasMemberInfo 28 | } 29 | 30 | @Throws(Exception::class) 31 | override fun resolveArgument( 32 | parameter: MethodParameter, 33 | mavContainer: ModelAndViewContainer?, 34 | webRequest: NativeWebRequest, 35 | binderFactory: WebDataBinderFactory?, 36 | ): MemberInfo { 37 | 38 | return if (profile.uppercase() == PROFILE_LOCAL) { 39 | MemberInfo(1, Role.USER) 40 | } else { 41 | val request = webRequest.nativeRequest as HttpServletRequest 42 | val accessTokenCookie = cookieProvider.getCookie(request.cookies, JwtTokenType.ACCESS.cookieName) 43 | ?: throw NotAccessTokenTypeException() 44 | val accessToken = accessTokenCookie.value 45 | 46 | val tokenClaims = tokenManager.getTokenClaims(accessToken) 47 | val memberId = (tokenClaims["memberId"] as Int).toLong() 48 | val role = tokenClaims["role"] as String 49 | 50 | MemberInfo(memberId, Role.from(role)) 51 | } 52 | 53 | } 54 | 55 | companion object { 56 | private const val PROFILE_LOCAL = "LOCAL" 57 | } 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/main/kotlin/app/askresume/api/access/usecase/OauthUseCase.kt: -------------------------------------------------------------------------------- 1 | package app.askresume.api.access.usecase 2 | 3 | import app.askresume.domain.member.constant.MemberType 4 | import app.askresume.domain.member.service.MemberCommandService 5 | import app.askresume.domain.member.service.MemberReadOnlyService 6 | import app.askresume.global.jwt.dto.JwtResponse 7 | import app.askresume.global.jwt.service.TokenManager 8 | import app.askresume.oauth.constant.OAuthProvider 9 | import app.askresume.oauth.service.OAuthService 10 | import org.springframework.stereotype.Service 11 | import org.springframework.transaction.annotation.Transactional 12 | import java.net.URI 13 | 14 | @Service 15 | @Transactional(readOnly = true) 16 | class OauthUseCase( 17 | private val tokenManager: TokenManager, 18 | private val memberReadOnlyService: MemberReadOnlyService, 19 | private val memberCommandService: MemberCommandService, 20 | private val oAuthService: OAuthService, 21 | ) { 22 | 23 | fun getAuthorizationUri(provider: OAuthProvider): URI { 24 | return oAuthService.authorize(provider) 25 | } 26 | 27 | @Transactional 28 | fun joinOrLogin(code: String, provider: OAuthProvider): JwtResponse.TokenSet { 29 | // OAuth Access 토큰을 얻어옵니다. 30 | val oAuthTokenDto = oAuthService.getToken(code, provider) 31 | // OAuth Access 토큰으로 OAuth UserInfo를 얻어옵니다. 32 | val userInfo = oAuthService.getUserInfo(oAuthTokenDto.accessToken, provider) 33 | 34 | // 해당 email과 provider 정보로 가입된 회원이 있는지 체크합니다. 35 | val memberType = MemberType.from(provider.name) 36 | val memberInfoDto = memberReadOnlyService.findMemberByEmail(userInfo.email, memberType) 37 | 38 | // 가입된 회원 정보가 있으면 로그인을, 없으면 회원가입을 진행합니다. 39 | val oAuthMember = memberInfoDto?.let { // 로그인 40 | memberInfoDto 41 | } ?: run { // 회원가입 42 | 43 | val memberId = memberCommandService.registerMember( 44 | email = userInfo.email, 45 | name = userInfo.name, 46 | profile = userInfo.profile, 47 | locale = userInfo.locale, 48 | memberType = memberType, 49 | ) 50 | 51 | memberReadOnlyService.findMemberInfo(memberId) !! 52 | } 53 | 54 | // 토큰 생성 55 | val jwtTokenSet = tokenManager.createJwtTokenSet(oAuthMember.memberId, oAuthMember.role) 56 | 57 | memberCommandService.updateRefreshToken( 58 | memberId = oAuthMember.memberId, 59 | refreshToken = jwtTokenSet.refreshToken.token, 60 | expireDate = jwtTokenSet.refreshToken.expireDate 61 | ) 62 | 63 | return jwtTokenSet 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | --------------------------------------------------------------------------------