Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion docs/plan/#18-goal-task-stats/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
- [x] 4단계: Infrastructure — ExposedTaskRepository에 집계 쿼리 구현
- [x] 5단계: Presentation — GoalTaskStatsDataLoader + GoalTaskStatsDataFetcher 구현
- [x] 6단계: 전체 테스트 통과 확인 및 검증
- [ ] 7단계: 리팩토링 — 집계 로직을 Infrastructure에서 Application으로 이동
- [x] 7단계: 리팩토링 — 집계 로직을 Infrastructure에서 Application으로 이동
10 changes: 10 additions & 0 deletions docs/plan/#36-me-query/checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 로그인 회원 본인 정보 조회 (me) Query 검증 체크리스트

## 필수 항목
- [x] 아키텍처 원칙 준수 (docs/architecture.md 기준)
- [x] 레이어 의존성 규칙 위반 없음
- [x] 테스트 코드 작성 완료 (Domain — 해당 없음, Application 필수)
- [x] 모든 테스트 통과
- [x] 기존 테스트 깨지지 않음
- [x] password 필드가 GraphQL 응답에 노출되지 않음
- [x] DGS Codegen으로 GraphQL 타입 자동 생성 (수동 작성 없음)
13 changes: 13 additions & 0 deletions docs/plan/#36-me-query/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 로그인 회원 본인 정보 조회 (me) Query 구현

> Issue: #36

## 단계

- [x] 1단계: GraphQL 스키마에 Member 타입과 me Query 추가
- [x] 2단계: DGS Codegen 실행하여 Member 타입 생성
- [x] 3단계: Domain — MemberRepository에 findById 메서드 추가 (TDD)
- [x] 4단계: Application — AuthService에 getMe 메서드 추가 (TDD)
- [x] 5단계: Infrastructure — ExposedMemberRepository에 findById 구현
- [x] 6단계: Presentation — AuthDataFetcher에 me Query 추가
- [x] 7단계: 전체 테스트 통과 확인
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package kr.io.team.loop.auth.application.service

import kr.io.team.loop.auth.application.dto.AuthTokenDto
import kr.io.team.loop.auth.domain.model.Member
import kr.io.team.loop.auth.domain.model.MemberCommand
import kr.io.team.loop.auth.domain.repository.MemberRepository
import kr.io.team.loop.common.config.JwtTokenProvider
import kr.io.team.loop.common.domain.MemberId
import kr.io.team.loop.common.domain.exception.AuthenticationException
import kr.io.team.loop.common.domain.exception.DuplicateEntityException
import kr.io.team.loop.common.domain.exception.EntityNotFoundException
Expand Down Expand Up @@ -39,4 +41,9 @@ class AuthService(
val token = jwtTokenProvider.generateToken(member.id.value)
return AuthTokenDto(accessToken = token)
}

@Transactional(readOnly = true)
fun getMe(memberId: MemberId): Member =
memberRepository.findById(memberId)
?: throw EntityNotFoundException("Member not found: ${memberId.value}")
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package kr.io.team.loop.auth.domain.repository
import kr.io.team.loop.auth.domain.model.LoginId
import kr.io.team.loop.auth.domain.model.Member
import kr.io.team.loop.auth.domain.model.MemberCommand
import kr.io.team.loop.common.domain.MemberId

interface MemberRepository {
fun save(command: MemberCommand.Register): Member

fun findById(id: MemberId): Member?

fun findByLoginId(loginId: LoginId): Member?

fun existsByLoginId(loginId: LoginId): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ class ExposedMemberRepository : MemberRepository {
)
}

override fun findById(id: MemberId): Member? =
MemberTable
.selectAll()
.where { MemberTable.memberId eq id.value }
.singleOrNull()
?.toMember()

override fun findByLoginId(loginId: LoginId): Member? =
MemberTable
.selectAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,29 @@ package kr.io.team.loop.auth.presentation.datafetcher

import com.netflix.graphql.dgs.DgsComponent
import com.netflix.graphql.dgs.DgsMutation
import com.netflix.graphql.dgs.DgsQuery
import com.netflix.graphql.dgs.InputArgument
import kr.io.team.loop.auth.application.service.AuthService
import kr.io.team.loop.auth.domain.model.LoginId
import kr.io.team.loop.auth.domain.model.Member
import kr.io.team.loop.auth.domain.model.MemberCommand
import kr.io.team.loop.auth.domain.model.Nickname
import kr.io.team.loop.codegen.types.AuthToken
import kr.io.team.loop.codegen.types.LoginInput
import kr.io.team.loop.codegen.types.RegisterInput
import kr.io.team.loop.common.config.Authorize
import kr.io.team.loop.common.domain.MemberId
import kr.io.team.loop.codegen.types.Member as MemberGraphql

@DgsComponent
class AuthDataFetcher(
private val authService: AuthService,
) {
@DgsQuery
fun me(
@Authorize memberId: Long,
): MemberGraphql = authService.getMe(MemberId(memberId)).toGraphql()

@DgsMutation
fun register(
@InputArgument input: RegisterInput,
Expand All @@ -41,4 +51,13 @@ class AuthDataFetcher(
val result = authService.login(command)
return AuthToken(accessToken = result.accessToken)
}

private fun Member.toGraphql(): MemberGraphql =
MemberGraphql(
id = id.value.toString(),
loginId = loginId.value,
nickname = nickname.value,
createdAt = createdAt.toString(),
updatedAt = updatedAt?.toString(),
)
}
19 changes: 19 additions & 0 deletions src/main/resources/schema/auth.graphqls
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
extend type Query {
"로그인된 회원 본인의 정보를 조회한다. 인증 필수."
me: Member!
}

extend type Mutation {
"회원가입 후 액세스 토큰을 발급한다."
register(input: RegisterInput!): AuthToken!
Expand Down Expand Up @@ -29,3 +34,17 @@ input LoginInput {
"비밀번호"
password: String!
}

"회원 정보"
type Member {
"회원 고유 식별자"
id: ID!
"로그인 ID"
loginId: String!
"닉네임"
nickname: String!
"가입 일시 (ISO-8601)"
createdAt: String!
"최근 수정 일시 (ISO-8601, 수정 이력이 없으면 null)"
updatedAt: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,28 @@ class AuthServiceTest :
}
}
}

Given("내 정보 조회 시") {
When("존재하는 회원이면") {
every { memberRepository.findById(MemberId(1L)) } returns savedMember

val result = authService.getMe(MemberId(1L))

Then("회원 정보를 반환한다") {
result.id shouldBe MemberId(1L)
result.loginId shouldBe LoginId("testuser")
result.nickname shouldBe Nickname("홍길동")
}
}

When("존재하지 않는 회원이면") {
every { memberRepository.findById(MemberId(999L)) } returns null

Then("예외가 발생한다") {
shouldThrow<EntityNotFoundException> {
authService.getMe(MemberId(999L))
}
}
}
}
})
Loading