Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c3785d6
feat: Spring Security 및 OAuth 관련 의존성 추가 (#42)
toychip Mar 25, 2026
e58def8
feat: Member 도메인 모델 OAuth 필드로 변경 (#42)
toychip Mar 25, 2026
5fe1182
feat: Member 영속성 계층 OAuth 필드 반영 (#42)
toychip Mar 25, 2026
6a8aca6
feat: OAuth output port 및 ExceptionCode 추가 (#42)
toychip Mar 25, 2026
11ce6c2
feat: Kakao, Google, Apple OAuth 어댑터 구현 (#42)
toychip Mar 25, 2026
0c4f905
feat: Spring Security 인프라 구성 (#42)
toychip Mar 25, 2026
23be245
feat: AuthController 소셜 로그인 엔드포인트 전환 (#42)
toychip Mar 25, 2026
ea0c7d2
feat: AuthService / MemberService 책임 분리 (#42)
toychip Mar 25, 2026
255158b
refactor: 기존 서블릿 필터 인증 코드 제거 및 SecurityContextHolder 전환 (#42)
toychip Mar 25, 2026
2352de6
test: OAuth 소셜 로그인 전환에 따른 테스트 수정 (#42)
toychip Mar 25, 2026
43134d6
refactor: ktlint 파일명-클래스명 불일치 수정 (#42)
toychip Mar 25, 2026
f25781c
refactor: ktlint 파일명-클래스명 불일치 수정 (#42)
toychip Mar 25, 2026
5478519
fix: JwtSecurityFilter에서 filterChain.doFilter를 try 밖으로 분리 (#42)
toychip Mar 25, 2026
a423633
fix: JwtAuthenticationProvider에서 모든 예외를 BadCredentialsException으로 변환 …
toychip Mar 25, 2026
da2553e
fix: SecurityFilterChain에 @Order 추가하여 평가 순서 보장 (#42)
toychip Mar 25, 2026
3194d0a
fix: SecurityPaths 공개 경로 범위 축소 (#42)
toychip Mar 25, 2026
a9981ec
fix: JwtTokenProvider에 IllegalArgumentException 예외 처리 추가 (#42)
toychip Mar 25, 2026
7da5274
fix: Apple ID token issuer/audience 검증 추가 (#42)
toychip Mar 25, 2026
fc5db1f
fix: MemberAdapter 동시 요청 중복 회원 저장 방지 (#42)
toychip Mar 25, 2026
2510ef5
fix: AuthService 로그에서 민감 식별자 제거 (#42)
toychip Mar 25, 2026
2cfc41a
refactor: 중복 인덱스 및 미사용 findByEmail 제거 (#42)
toychip Mar 25, 2026
493375e
fix: AppleOAuthAdapter 토큰 파싱 예외 처리 및 providerId 검증 추가 (#42)
toychip Mar 25, 2026
bd33396
fix: 예외 타입 포맷에 맞게 수정 (#43)
toychip Mar 30, 2026
a37f999
fix: 예외 타입 포맷에 맞게 수정 (#43)
toychip Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 3 additions & 67 deletions .github/workflows/ci-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,14 @@ name: CI - Pull request
on:
pull_request:
branches: [ main ]
issue_comment:
types: [ created ]

permissions:
contents: read
issues: write
pull-requests: write

jobs:
auto-check:
name: 자동 검증 (ktlint + test)
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' }}
steps:
- name: Repository 접근
uses: actions/checkout@v4
Expand Down Expand Up @@ -43,66 +38,7 @@ jobs:
run: ./gradlew ktlintCheck

- name: 테스트 실행
run: ./gradlew test
run: ./gradlew :linktrip-application:test

build:
name: ready for launch (CI)
runs-on: ubuntu-latest
if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '빌드검증')}}
steps:
- name: Get PR branch
id: get_branch
run: |
PR_NUMBER=$(jq --raw-output .issue.number "$GITHUB_EVENT_PATH")
BRANCH_NAME=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER" | \
jq -r .head.ref)
echo "branch=$BRANCH_NAME" >> $GITHUB_OUTPUT

- name: Repository 접근
uses: actions/checkout@v4
with:
ref: ${{ steps.get_branch.outputs.branch }}

- name: JDK 21 셋팅
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'

- name: Gradle 의존성 캐싱
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: ktlint check
run: ./gradlew ktlintCheck

- name: 프로젝트 빌드 (테스트 포함)
run: ./gradlew build

- name: Set build status output
id: set_status
run: echo "build_status=${{ job.status }}" >> $GITHUB_OUTPUT

- name: Comment on Pull Request
if: always()
uses: actions/github-script@v7
with:
script: |
const status = '${{ steps.set_status.outputs.build_status }}';
const message = status === 'success' ? '빌드 성공 \n‍🚀' : '❌❌ 빌드 실패 ❌❌\n 코드 확인이 필요해요😵‍💫';
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `${message}`
});
- name: 프로젝트 빌드
run: ./gradlew build -x test
5 changes: 3 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ kotlin-logging = { group = "io.github.microutils", name = "kotlin-logging-jvm",
# Spring
spring-web = { group = "org.springframework", name = "spring-web" }
spring-transaction = { group = "org.springframework", name = "spring-tx" }
spring-boot-starter-security = { group = "org.springframework.boot", name = "spring-boot-starter-security" }

spring-boot-starter-autoconfigure = { group = "org.springframework.boot", name = "spring-boot-autoconfigure" }
spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
Expand Down Expand Up @@ -88,10 +89,10 @@ test-runtime = ["junit"]
# 모듈별
bootstrap = ["spring-boot-starter-web", "spring-boot-starter-actuator"]
domain-application = ["spring-boot-docs", "spring-transaction", "kotlinx-coroutines-core"]
adaptor-input-http = ["spring-boot-starter-web", "spring-boot-starter-aop", "spring-boot-docs", "spring-boot-starter-validation", "jjwt-api", "jjwt-impl", "jjwt-jackson"]
adaptor-input-http = ["spring-boot-starter-web", "spring-boot-starter-aop", "spring-boot-docs", "spring-boot-starter-validation", "spring-boot-starter-security", "jjwt-api", "jjwt-impl", "jjwt-jackson"]
adaptor-persistence-mysql = ["spring-boot-starter-jpa", "mysql"]
adaptor-cache-redis = ["spring-boot-starter-cache", "spring-boot-starter-redis"]
adaptor-cache-caffeine = ["spring-boot-starter-cache", "caffeine", "bucket4j-core"]
adaptor-storage-aws = ["spring-web", "aws-sdk-s3"]
adaptor-output-http = ["spring-web", "google-genai"]
adaptor-output-http = ["spring-web", "google-genai", "jjwt-api", "jjwt-impl", "jjwt-jackson"]
adaptor-input-batch = ["spring-boot-starter-batch"]
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,40 @@ package com.linktrip.application.domain.member

import com.linktrip.application.port.input.AuthUseCase
import com.linktrip.application.port.input.AuthUseCase.AuthResult
import com.linktrip.application.port.output.auth.OAuthPort
import com.linktrip.application.port.output.auth.TokenProvider
import com.linktrip.application.port.output.persistence.MemberPort
import com.linktrip.common.exception.ExceptionCode
import com.linktrip.common.exception.LinktripException
import mu.KotlinLogging
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

private val logger = KotlinLogging.logger {}

@Service
class AuthService(
private val memberPort: MemberPort,
private val oAuthPorts: List<OAuthPort>,
private val memberService: MemberService,
private val tokenProvider: TokenProvider,
) : AuthUseCase {
@Transactional
override fun authenticateBySerial(serialNumber: String): AuthResult {
val existingMember = memberPort.findBySerialNumber(serialNumber)

val member =
existingMember ?: memberPort.save(
Member.create(serialNumber = serialNumber),
)
override fun socialLogin(
providerType: ProviderType,
accessToken: String,
): AuthResult {
val oAuthPort = getOAuthPort(providerType)
val oAuthInfo = oAuthPort.requestUserInfo(accessToken)
logger.info { "OAuth 사용자 정보 조회 완료: provider=$providerType" }

val accessToken = tokenProvider.create(member.id)
val result = memberService.findOrCreateByOAuth(providerType, oAuthInfo)
val token = tokenProvider.create(result.member.id)

return AuthResult(
memberId = member.id,
accessToken = accessToken,
isNewMember = existingMember == null,
memberId = result.member.id,
accessToken = token,
isNewMember = result.isNewMember,
)
}

private fun getOAuthPort(providerType: ProviderType): OAuthPort =
oAuthPorts.find { it.getProviderType() == providerType }
?: throw LinktripException(ExceptionCode.BAD_REQUEST_OAUTH_PROVIDER, providerType.name)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,23 @@ import java.time.LocalDateTime

data class Member(
val id: String,
val serialNumber: String,
val email: String?,
val providerType: ProviderType,
val providerId: String,
val createdAt: LocalDateTime = LocalDateTime.now(),
val updatedAt: LocalDateTime = LocalDateTime.now(),
) {
companion object {
fun create(serialNumber: String): Member =
fun create(
email: String?,
providerType: ProviderType,
providerId: String,
): Member =
Member(
id = IdGenerator.generate(),
serialNumber = serialNumber,
email = email,
providerType = providerType,
providerId = providerId,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.linktrip.application.domain.member

import com.linktrip.application.port.output.auth.OAuthInfo
import com.linktrip.application.port.output.persistence.MemberPort
import mu.KotlinLogging
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

private val logger = KotlinLogging.logger {}

@Service
class MemberService(
private val memberPort: MemberPort,
) {
@Transactional
fun findOrCreateByOAuth(
providerType: ProviderType,
oAuthInfo: OAuthInfo,
): MemberLoginResult {
memberPort.findByProviderTypeAndProviderId(providerType, oAuthInfo.providerId)
?.let { return MemberLoginResult(member = it, isNewMember = false) }

val newMember =
memberPort.save(
Member.create(
email = oAuthInfo.email,
providerType = providerType,
providerId = oAuthInfo.providerId,
),
)
Comment thread
toychip marked this conversation as resolved.

logger.info { "신규 회원 생성: memberId=${newMember.id}, provider=$providerType" }
return MemberLoginResult(member = newMember, isNewMember = true)
}

data class MemberLoginResult(
val member: Member,
val isNewMember: Boolean,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.linktrip.application.domain.member

enum class ProviderType {
KAKAO,
GOOGLE,
APPLE,
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.linktrip.application.port.input

import com.linktrip.application.domain.member.ProviderType

interface AuthUseCase {
fun authenticateBySerial(serialNumber: String): AuthResult
fun socialLogin(
providerType: ProviderType,
accessToken: String,
): AuthResult

data class AuthResult(
val memberId: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.linktrip.application.port.output.auth

import com.linktrip.application.domain.member.ProviderType

data class OAuthInfo(
val providerType: ProviderType,
val providerId: String,
val email: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.linktrip.application.port.output.auth

import com.linktrip.application.domain.member.ProviderType

interface OAuthPort {
fun getProviderType(): ProviderType

fun requestUserInfo(accessToken: String): OAuthInfo
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.linktrip.application.port.output.persistence

import com.linktrip.application.domain.member.Member
import com.linktrip.application.domain.member.ProviderType

interface MemberPort {
fun findBySerialNumber(serialNumber: String): Member?
fun findByProviderTypeAndProviderId(
providerType: ProviderType,
providerId: String,
): Member?

fun save(member: Member): Member
}
Loading
Loading