diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/in/web/PassInfoController.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/in/web/PassInfoController.kt new file mode 100644 index 0000000..5c7db04 --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/in/web/PassInfoController.kt @@ -0,0 +1,40 @@ +package hs.kr.entrydsm.user.domain.auth.adapter.`in`.web + +import hs.kr.entrydsm.user.domain.auth.adapter.`in`.web.dto.request.PassPopupRequest +import hs.kr.entrydsm.user.domain.auth.adapter.`in`.web.dto.resopnse.QueryPassInfoResponse +import hs.kr.entrydsm.user.domain.auth.application.port.`in`.PassPopupUseCase +import hs.kr.entrydsm.user.domain.auth.application.port.`in`.QueryPassInfoUseCase +import hs.kr.entrydsm.user.global.document.auth.AuthApiDocument +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +/** + * 패스 인증 관련 HTTP 요청을 처리하는 REST 컨트롤러 클래스입니다. + */ +@RestController +@RequestMapping("/user/verify") +class PassInfoController( + private val passPopupUseCase: PassPopupUseCase, + private val queryPassInfoUseCase: QueryPassInfoUseCase, +) : AuthApiDocument { + /** + * 패스 인증 정보를 조회합니다. + */ + @GetMapping("/info") + override fun getPassInfo( + @RequestParam("mdl_tkn") token: String, + ): QueryPassInfoResponse = queryPassInfoUseCase.queryPassInfo(token) + + /** + * 패스 인증 팝업을 생성합니다. + */ + @PostMapping("/popup") + override fun popupPass( + @RequestBody request: @Valid PassPopupRequest, + ): String = passPopupUseCase.generatePopup(request) +} diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/in/web/dto/request/PassPopupRequest.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/in/web/dto/request/PassPopupRequest.kt new file mode 100644 index 0000000..0ecc451 --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/in/web/dto/request/PassPopupRequest.kt @@ -0,0 +1,11 @@ +package hs.kr.entrydsm.user.domain.auth.adapter.`in`.web.dto.request + +import jakarta.validation.constraints.NotBlank + +/** + * 패스 팝업 생성 요청 데이터를 담는 DTO 클래스입니다. + */ +data class PassPopupRequest( + @NotBlank(message = "redirect_url은 Null 또는 공백 또는 띄어쓰기를 허용하지 않습니다.") + val redirectUrl: String, +) diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/in/web/dto/resopnse/QueryPassInfoResponse.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/in/web/dto/resopnse/QueryPassInfoResponse.kt new file mode 100644 index 0000000..b50213e --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/in/web/dto/resopnse/QueryPassInfoResponse.kt @@ -0,0 +1,9 @@ +package hs.kr.entrydsm.user.domain.auth.adapter.`in`.web.dto.resopnse + +/** + * 패스 인증 정보 조회 응답 데이터를 담는 DTO 클래스입니다. + */ +data class QueryPassInfoResponse( + val phoneNumber: String, + val name: String, +) diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/out/PassInfo.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/out/PassInfo.kt new file mode 100644 index 0000000..7ca1f9b --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/out/PassInfo.kt @@ -0,0 +1,24 @@ +package hs.kr.entrydsm.user.domain.auth.adapter.out + +import org.springframework.data.annotation.Id +import org.springframework.data.redis.core.RedisHash +import org.springframework.data.redis.core.TimeToLive +import org.springframework.data.redis.core.index.Indexed + +/** + * Redis에 저장되는 Pass 인증 정보를 나타내는 클래스입니다. + * Pass 인증을 통해 검증된 사용자 정보를 임시로 저장합니다. + * + * @property phoneNumber 암호화된 전화번호 (Redis 키로 사용) + * @property name 암호화된 사용자 이름 + * @property ttl Time To Live (데이터 만료 시간, 초 단위) + */ +@RedisHash +class PassInfo( + @Id + val phoneNumberHash: String, + val phoneNumber: String, + val name: String, + @TimeToLive + val ttl: Long +) diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/out/repository/PassInfoRepository.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/out/repository/PassInfoRepository.kt new file mode 100644 index 0000000..cd2496c --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/adapter/out/repository/PassInfoRepository.kt @@ -0,0 +1,13 @@ +package hs.kr.entrydsm.user.domain.auth.adapter.out.repository + +import hs.kr.entrydsm.user.domain.auth.adapter.out.PassInfo +import org.springframework.data.repository.CrudRepository +import java.util.Optional + +/** + * Pass 인증 정보를 위한 Redis 저장소 인터페이스입니다. + * Spring Data Redis를 통해 Pass 인증 데이터의 관리를 담당합니다. + */ +interface PassInfoRepository : CrudRepository { + +} diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/application/port/in/PassPopupUseCase.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/application/port/in/PassPopupUseCase.kt new file mode 100644 index 0000000..c6ceefc --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/application/port/in/PassPopupUseCase.kt @@ -0,0 +1,13 @@ +package hs.kr.entrydsm.user.domain.auth.application.port.`in` + +import hs.kr.entrydsm.user.domain.auth.adapter.`in`.web.dto.request.PassPopupRequest + +/** + * 패스 팝업 생성 기능을 정의하는 UseCase 인터페이스입니다. + */ +interface PassPopupUseCase { + /** + * 패스 인증 팝업을 생성합니다. + */ + fun generatePopup(request: PassPopupRequest): String +} diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/application/port/in/QueryPassInfoUseCase.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/application/port/in/QueryPassInfoUseCase.kt new file mode 100644 index 0000000..ba6f694 --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/application/port/in/QueryPassInfoUseCase.kt @@ -0,0 +1,13 @@ +package hs.kr.entrydsm.user.domain.auth.application.port.`in` + +import hs.kr.entrydsm.user.domain.auth.adapter.`in`.web.dto.resopnse.QueryPassInfoResponse + +/** + * 패스 인증 정보 조회 기능을 정의하는 UseCase 인터페이스입니다. + */ +interface QueryPassInfoUseCase { + /** + * 토큰을 이용하여 패스 인증 정보를 조회합니다. + */ + fun queryPassInfo(token: String?): QueryPassInfoResponse +} diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/application/port/out/.gitkit b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/application/port/out/.gitkit new file mode 100644 index 0000000..e69de29 diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/application/service/PassPopupService.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/application/service/PassPopupService.kt new file mode 100644 index 0000000..b339263 --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/auth/application/service/PassPopupService.kt @@ -0,0 +1,114 @@ +package hs.kr.entrydsm.user.domain.auth.application.service + +import hs.kr.entrydsm.user.domain.auth.adapter.`in`.web.dto.request.PassPopupRequest +import hs.kr.entrydsm.user.domain.auth.application.port.`in`.PassPopupUseCase +import hs.kr.entrydsm.user.global.exception.InternalServerErrorException +import hs.kr.entrydsm.user.global.utils.pass.RedirectUrlChecker +import kcb.module.v3.OkCert +import org.json.JSONObject +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +/** + * 패스 팝업 생성 비즈니스 로직을 처리하는 서비스 클래스입니다. + */ +@Service +class PassPopupService( + private val redirectUrlChecker: RedirectUrlChecker, +) : PassPopupUseCase { + companion object { + private const val TARGET = "PROD" + private val logger = LoggerFactory.getLogger(PassPopupService::class.java) + } + + @Value("\${pass.site-name}") + private lateinit var siteName: String + + @Value("\${pass.site-url}") + private lateinit var siteUrl: String + + @Value("\${pass.popup-url}") + private lateinit var popupUrl: String + + @Value("\${pass.cp-cd}") + private lateinit var cpCd: String + + @Value("\${pass.license}") + private lateinit var license: String + + private val svcName = "IDS_HS_POPUP_START" + + private val rqstCausCd = "00" + + /** + * 패스 인증 팝업 HTML을 생성합니다. + */ + @Transactional + override fun generatePopup(passPopupRequest: PassPopupRequest): String { + redirectUrlChecker.checkRedirectUrl(passPopupRequest.redirectUrl) + try { + val reqJson = JSONObject() + reqJson.put("RETURN_URL", passPopupRequest.redirectUrl) + reqJson.put("SITE_NAME", siteName) + reqJson.put("SITE_URL", siteUrl) + reqJson.put("RQST_CAUS_CD", rqstCausCd) + + val reqStr: String = reqJson.toString() + + val okcert = OkCert() + + val resultStr: String = okcert.callOkCert(TARGET, cpCd, svcName, license, reqStr) + + val resJson = JSONObject(resultStr) + + val rsltCd: String = resJson.getString("RSLT_CD") + val rsltMsg: String = resJson.getString("RSLT_MSG") + var mdlTkn = "" + + var succ = false + + if ("B000" == rsltCd && resJson.has("MDL_TKN")) { + mdlTkn = resJson.getString("MDL_TKN") + succ = true + } + + val htmlBuilder = StringBuilder() + htmlBuilder.append("") + htmlBuilder.append("KCB 휴대폰 본인확인 서비스 샘플 2") + htmlBuilder.append("") + htmlBuilder.append("") + htmlBuilder.append("") + htmlBuilder.append("") + htmlBuilder.append("") + htmlBuilder.append("
") + htmlBuilder.append( + "", + ) + htmlBuilder.append("") + htmlBuilder.append("") + htmlBuilder.append("") + htmlBuilder.append("
") + htmlBuilder.append("") + htmlBuilder.append("") + htmlBuilder.append("") + + return htmlBuilder.toString() + } catch (e: Exception) { + logger.error("Pass popup 생성 중 오류 발생: ${e.message}", e) + throw InternalServerErrorException + } + } +} diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/refreshtoken/adapter/out/RefreshToken.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/refreshtoken/adapter/out/RefreshToken.kt new file mode 100644 index 0000000..a261008 --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/refreshtoken/adapter/out/RefreshToken.kt @@ -0,0 +1,38 @@ +package hs.kr.entrydsm.user.domain.refreshtoken.adapter.out + +import org.springframework.data.annotation.Id +import org.springframework.data.redis.core.RedisHash +import org.springframework.data.redis.core.TimeToLive +import org.springframework.data.redis.core.index.Indexed + +/** + * Redis에 저장되는 리프레시 토큰 정보를 나타내는 클래스입니다. + * JWT 리프레시 토큰을 관리하여 토큰 갱신을 처리합니다. + * + * @property id 사용자 ID (Redis 키로 사용) + * @property token 리프레시 토큰 값 + * @property ttl Time To Live (토큰 만료 시간, 초 단위) + */ +@RedisHash +class RefreshToken( + @Id + val id: String, + @Indexed + var token: String, + @TimeToLive + var ttl: Long, +) { + /** + * 리프레시 토큰과 TTL을 업데이트합니다. + * + * @param token 새로운 리프레시 토큰 + * @param ttl 새로운 만료 시간 (초) + */ + fun update( + token: String, + ttl: Long, + ) { + this.token = token + this.ttl = ttl + } +} diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/refreshtoken/adapter/out/repository/RefreshTokenRepository.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/refreshtoken/adapter/out/repository/RefreshTokenRepository.kt new file mode 100644 index 0000000..d0ee8f5 --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/refreshtoken/adapter/out/repository/RefreshTokenRepository.kt @@ -0,0 +1,14 @@ +package hs.kr.entrydsm.user.domain.refreshtoken.adapter.out.repository + +import hs.kr.entrydsm.user.domain.refreshtoken.adapter.out.RefreshToken +import org.springframework.data.repository.CrudRepository + +/** + * 리프레시 토큰에 대한 Redis 액세스를 담당하는 리포지토리 인터페이스입니다. + */ +interface RefreshTokenRepository : CrudRepository { + /** + * 토큰 값으로 리프레시 토큰을 조회합니다. + */ + fun findByToken(token: String): RefreshToken? +}