Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: problem 도메인 1차 구현 #49

Merged
merged 23 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
860b067
feat: problem 도메인 임시 구현 (미완, 임시 커밋)
hun-ca Jun 17, 2024
9f36423
refactor: data-jdbc 의존성을 api 방식으로 변경
belljun3395 Jun 18, 2024
1097703
feat: api 모듈에 api-repo 모듈 의존성 추가
belljun3395 Jun 18, 2024
7c8a6db
feat: ApiConfig 구현
belljun3395 Jun 18, 2024
f523d59
feat: api 모듈에 api-repo 모듈 빈 임포트
belljun3395 Jun 18, 2024
a10e990
feat: application.yml 추가
belljun3395 Jun 19, 2024
67bb5a4
Merge remote-tracking branch 'origin/feat/#50_belljun3395' into feat/…
hun-ca Jun 19, 2024
33c69bc
fix: 문제 컨텐츠(json string) to Object 변환 과정 추가
hun-ca Jun 19, 2024
77a9ab9
feat: 문제 정답 확인 API 1차 구현
hun-ca Jun 19, 2024
699991e
refactor: problem 도메인 리펙토링
hun-ca Jun 19, 2024
a673c63
refactor: problem 도메인 api-repo 모듈 리펙토링
hun-ca Jun 19, 2024
ef9895d
refactor: rename ReadProblemContentsUseCaseOut -> ReadProblemContents…
hun-ca Jun 22, 2024
3e0be56
fix: remove duplicated application resource file
hun-ca Jun 22, 2024
26717cb
fix: remove DB schema migration file
hun-ca Jun 22, 2024
0e3bd80
chore: DB 스키마 마이그레이션 파일 gitignore 등록
hun-ca Jun 22, 2024
77654e4
refactor: jooq select 결과를 바로 class로 매핑하도록 수정
hun-ca Jun 22, 2024
9c23cae
refactor: DAO 메소드 이름 수정 (~~By~~)
hun-ca Jun 22, 2024
f4d1da7
test: usecase mocking 적용
hun-ca Jun 22, 2024
e0f0f30
chore: rename test method name
hun-ca Jun 22, 2024
9c7ae3e
fix: SQL에 deleted_at is null 조건 추가
hun-ca Jun 23, 2024
89655f3
test: 모킹 인풋 객체 추가
hun-ca Jun 23, 2024
72795d8
refactor: DAO 메소드 명 수정
hun-ca Jun 23, 2024
20141ce
chore: merge from main (resolve conflict)
hun-ca Jun 23, 2024
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
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,10 @@ Footer
*.ear
*.zip
*.tar.gz
*.rar
*.rar

# Jooq
**/generated/
*.log
*.tmp

2 changes: 2 additions & 0 deletions api-repo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# DB schema migration path
**/*.sql
2 changes: 1 addition & 1 deletion api-repo/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ sourceSets {

dependencies {
/** spring starter */
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
api("org.springframework.boot:spring-boot-starter-data-jdbc")
implementation("com.mysql:mysql-connector-j")

/** flyway */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.few.api.repo.dao.problem

import com.few.api.repo.dao.problem.query.SelectProblemAnswerQuery
import com.few.api.repo.dao.problem.query.SelectProblemQuery
import com.few.api.repo.dao.problem.record.SelectProblemAnswerRecord
import com.few.api.repo.dao.problem.record.SelectProblemRecord
import jooq.jooq_dsl.tables.Problem
import org.jooq.DSLContext
import org.jooq.impl.DSL
import org.springframework.stereotype.Component

@Component
class ProblemDao(
hun-ca marked this conversation as resolved.
Show resolved Hide resolved
private val dslContext: DSLContext
) {
fun selectProblemContentsByProblemId(query: SelectProblemQuery): SelectProblemRecord {
return dslContext.select(
Problem.PROBLEM.ID.`as`(SelectProblemRecord::id.name),
Problem.PROBLEM.TITLE.`as`(SelectProblemRecord::title.name),
DSL.field("JSON_UNQUOTE({0})", String::class.java, Problem.PROBLEM.CONTENTS)
.`as`(SelectProblemRecord::contents.name)
)
.from(Problem.PROBLEM)
.where(Problem.PROBLEM.ID.eq(query.problemId))
.fetchOneInto(SelectProblemRecord::class.java)
?: throw RuntimeException("Problem with ID ${query.problemId} not found") // TODO: 에러 표준화
}

fun selectProblemAnswerByProblemId(query: SelectProblemAnswerQuery): SelectProblemAnswerRecord {
return dslContext.select(
Problem.PROBLEM.ID.`as`(SelectProblemAnswerRecord::id.name),
Problem.PROBLEM.ANSWER.`as`(SelectProblemAnswerRecord::answer.name),
Problem.PROBLEM.EXPLANATION.`as`(SelectProblemAnswerRecord::explanation.name)
)
.from(Problem.PROBLEM)
.where(Problem.PROBLEM.ID.eq(query.problemId))
.fetchOneInto(SelectProblemAnswerRecord::class.java)
?: throw RuntimeException("Problem Answer with ID ${query.problemId} not found") // TODO: 에러 표준화
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.few.api.repo.dao.problem

import com.few.api.repo.dao.problem.command.InsertSubmitHistoryCommand
import jooq.jooq_dsl.Tables.SUBMIT_HISTORY
import org.jooq.DSLContext
import org.springframework.stereotype.Component

@Component
class SubmitHistoryDao(
private val dslContext: DSLContext
) {

fun insertSubmitHistory(command: InsertSubmitHistoryCommand): Long {
val result = dslContext.insertInto(SUBMIT_HISTORY)
.set(SUBMIT_HISTORY.PROBLEM_ID, command.problemId)
.set(SUBMIT_HISTORY.MEMBER_ID, command.memberId)
.set(SUBMIT_HISTORY.SUBMIT_ANS, command.submitAns)
.set(SUBMIT_HISTORY.IS_SOLVED, command.isSolved)
.returning(SUBMIT_HISTORY.ID)
.fetchOne()

return result?.getValue(SUBMIT_HISTORY.ID)
?: throw RuntimeException("Submit History with ID ${command.problemId} insertion fail") // TODO: 에러 표준화
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.few.api.repo.dao.problem.command

data class InsertSubmitHistoryCommand(
val problemId: Long,
val memberId: Long,
val submitAns: String,
val isSolved: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.few.api.repo.dao.problem.query

data class SelectProblemAnswerQuery(
val problemId: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.few.api.repo.dao.problem.query

data class SelectProblemQuery(
val problemId: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.few.api.repo.dao.problem.record

data class SelectProblemAnswerRecord(
val id: Long,
val answer: String,
val explanation: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.few.api.repo.dao.problem.record

data class SelectProblemRecord(
val id: Long,
val title: String,
val contents: String
)
3 changes: 3 additions & 0 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import org.hidetake.gradle.swagger.generator.GenerateSwaggerUI

dependencies {
/** module */
implementation(project(":api-repo"))

/** spring starter */
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-actuator")
Expand Down
19 changes: 19 additions & 0 deletions api/src/main/kotlin/com/few/api/config/ApiConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.few.api.config

import com.few.api.repo.config.ApiRepoConfig
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import

@Configuration
@ComponentScan(basePackages = [ApiConfig.BASE_PACKAGE])
@Import(ApiRepoConfig::class)
class ApiConfig {
companion object {
const val BASE_PACKAGE = "com.few.api"
const val SERVICE_NAME = "few"
const val MODULE_NAME = "few-api"
const val BEAN_NAME_PREFIX = "fewApi"
const val PROPERTY_PREFIX = SERVICE_NAME + "." + MODULE_NAME
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,53 @@ import com.few.api.web.controller.problem.response.ProblemContents
import com.few.api.web.controller.problem.response.ReadProblemResponse
import com.few.api.web.support.ApiResponse
import com.few.api.web.support.ApiResponseGenerator
import com.few.api.web.usecase.problem.CheckProblemUseCase
import com.few.api.web.usecase.problem.ReadProblemUseCase
import com.few.api.web.usecase.problem.`in`.CheckProblemUseCaseIn
import com.few.api.web.usecase.problem.`in`.ReadProblemUseCaseIn
import org.springframework.http.HttpStatus
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*
import java.util.*

@Validated
@RestController
@RequestMapping("/api/v1/problems")
class ProblemController {
class ProblemController(
private val readProblemUseCase: ReadProblemUseCase,
private val checkProblemUseCase: CheckProblemUseCase
) {

@GetMapping("/{problemId}")
fun readProblem(
hun-ca marked this conversation as resolved.
Show resolved Hide resolved
@PathVariable(value = "problemId") problemId: Long
): ApiResponse<ApiResponse.SuccessBody<ReadProblemResponse>> {
val data = ReadProblemResponse(
id = 1L,
title = "ETF(상장지수펀드)의 특징이 아닌것은?",
contents = listOf(
ProblemContents(1L, "분산투자"),
ProblemContents(2L, "높은 운용 비용"),
ProblemContents(3L, "유동성"),
ProblemContents(4L, "투명성")
)
val useCaseOut = readProblemUseCase.execute(ReadProblemUseCaseIn(problemId))

val response = ReadProblemResponse(
id = useCaseOut.id,
title = useCaseOut.title,
contents = useCaseOut.contents
.map { c -> ProblemContents(c.number, c.content) }
.toCollection(LinkedList())
)
return ApiResponseGenerator.success(data, HttpStatus.OK)

return ApiResponseGenerator.success(response, HttpStatus.OK)
}

@PostMapping("/{problemId}")
hun-ca marked this conversation as resolved.
Show resolved Hide resolved
fun checkProblem(
@PathVariable(value = "problemId") problemId: Long,
@RequestBody body: CheckProblemBody
): ApiResponse<ApiResponse.SuccessBody<CheckProblemResponse>> {
val data = CheckProblemResponse(
explanation = "ETF는 일반적으로 낮은 운용 비용을 특징으로 합니다.이는 ETF가 보통 지수 추종(passive management) 방식으로 운용되기 때문입니다. 지수를 추종하는 전략은 액티브 매니지먼트(active management)에 비해 관리가 덜 복잡하고, 따라서 비용이 낮습니다.",
answer = "2",
isSolved = true
val useCaseOut = checkProblemUseCase.execute(CheckProblemUseCaseIn(problemId, body.sub))

val response = CheckProblemResponse(
explanation = useCaseOut.explanation,
answer = useCaseOut.answer,
isSolved = useCaseOut.isSolved
)
return ApiResponseGenerator.success(data, HttpStatus.OK)

return ApiResponseGenerator.success(response, HttpStatus.OK)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.few.api.web.usecase.problem

import com.few.api.repo.dao.problem.ProblemDao
import com.few.api.repo.dao.problem.SubmitHistoryDao
import com.few.api.repo.dao.problem.command.InsertSubmitHistoryCommand
import com.few.api.repo.dao.problem.query.SelectProblemAnswerQuery
import com.few.api.web.usecase.problem.`in`.CheckProblemUseCaseIn
import com.few.api.web.usecase.problem.out.CheckProblemUseCaseOut
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

@Component
class CheckProblemUseCase(
private val problemDao: ProblemDao,
private val submitHistoryDao: SubmitHistoryDao
) {

@Transactional
fun execute(useCaseIn: CheckProblemUseCaseIn): CheckProblemUseCaseOut {
val problemId = useCaseIn.problemId
val submitAns = useCaseIn.sub

val record = problemDao.selectProblemAnswerByProblemId(SelectProblemAnswerQuery(problemId))
val isSolved = submitAns.equals(record.answer)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

record.answer.equals(submitAns)

위와 같이 순서를 변경하거나

submitAns는 모델을 만들어 처리하는게 어떨까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아,, 이런거 생각하면 JPA가 좋긴 하구나...


val submitHistoryId = submitHistoryDao.insertSubmitHistory(
InsertSubmitHistoryCommand(problemId, 1L, submitAns, isSolved)
) // not used 'submitHistoryId'

hun-ca marked this conversation as resolved.
Show resolved Hide resolved
return CheckProblemUseCaseOut(
explanation = record.explanation,
isSolved = isSolved,
answer = record.answer
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.few.api.web.usecase.problem

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.few.api.repo.dao.problem.ProblemDao
import com.few.api.repo.dao.problem.query.SelectProblemQuery
import com.few.api.web.usecase.problem.`in`.ReadProblemUseCaseIn
import com.few.api.web.usecase.problem.out.ReadProblemContentsUseCaseOutDetail
import com.few.api.web.usecase.problem.out.ReadProblemUseCaseOut
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

@Component
class ReadProblemUseCase(
private val problemDao: ProblemDao,
private val objectMapper: ObjectMapper
) {

@Transactional(readOnly = true)
fun execute(useCaseIn: ReadProblemUseCaseIn): ReadProblemUseCaseOut {
val problemId = useCaseIn.problemId

val record = problemDao.selectProblemContentsByProblemId(SelectProblemQuery(problemId))

val contents: List<ReadProblemContentsUseCaseOutDetail> = objectMapper.readValue(record.contents)

return ReadProblemUseCaseOut(
id = record.id,
title = record.title,
contents = contents
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.few.api.web.usecase.problem.`in`

data class CheckProblemUseCaseIn(
val problemId: Long,
val sub: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.few.api.web.usecase.problem.`in`

data class ReadProblemUseCaseIn(
val problemId: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.few.api.web.usecase.problem.out

data class CheckProblemUseCaseOut(
val explanation: String,
val answer: String,
val isSolved: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.few.api.web.usecase.problem.out

class ReadProblemUseCaseOut(
val id: Long,
val title: String,
val contents: List<ReadProblemContentsUseCaseOutDetail>
)

data class ReadProblemContentsUseCaseOutDetail(
val number: Long,
val content: String
)
10 changes: 10 additions & 0 deletions api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
spring:
profiles:
group:
local:
- api-repo-local
prd:
- api-repo-prd
test:
- new
- api-repo-local
Loading
Loading