diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
new file mode 100644
index 0000000..c9608e6
--- /dev/null
+++ b/.github/workflows/CI.yml
@@ -0,0 +1,38 @@
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created
+# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle
+
+name: Gradle Package
+
+on:
+ pull_request:
+ branches:
+ - "master"
+ - "develop"
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: checkout
+ uses: actions/checkout@v3
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+
+
+ - name: build and test
+ run: |
+ chmod +x gradlew
+ ./gradlew build
diff --git a/.gitignore b/.gitignore
index 4125023..7c43360 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,8 @@ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
-
.env
+local.env
### STS ###
.apt_generated
@@ -37,3 +37,5 @@ out/
### VS Code ###
.vscode/
+
+src/test/resources/insert-search-problem.sql
diff --git a/Dockerfile b/Dockerfile
index 780a42f..a90d3c3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,17 +2,6 @@ FROM openjdk:17
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
-ARG apm_agent=apm-agent/*.jar
-COPY ${apm_agent} apm-agent.jar
-
-ARG PROFILE=dev
ENV SPRING_PROFILES_ACTIVE=${PROFILE}
-ENTRYPOINT ["java", \
-"-javaagent:/apm-agent.jar", \
-"-Delastic.apm.server_urls=http://114.70.23.79:8200", \
-"-Delastic.apm.service_name=moplus-apm-agent", \
-"-Delastic.apm.application_packages=com.server", \
-"-Delastic.apm.environment=dev", \
-"-jar", \
-"/app.jar"]
\ No newline at end of file
+ENTRYPOINT ["java", "-jar", "app.jar"]
\ No newline at end of file
diff --git a/Dockerfile.prod b/Dockerfile.prod
index 5f66081..a90d3c3 100644
--- a/Dockerfile.prod
+++ b/Dockerfile.prod
@@ -2,17 +2,6 @@ FROM openjdk:17
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
-ARG apm_agent=apm-agent/*.jar
-COPY ${apm_agent} apm-agent.jar
-
-ARG PROFILE=dev
ENV SPRING_PROFILES_ACTIVE=${PROFILE}
-ENTRYPOINT ["java", \
-"-javaagent:/apm-agent.jar", \
-"-Delastic.apm.server_urls=http://114.70.23.79:8200", \
-"-Delastic.apm.service_name=moplus-apm-agent", \
-"-Delastic.apm.application_packages=com.server", \
-"-Delastic.apm.environment=prod", \
-"-jar", \
-"/app.jar"]
\ No newline at end of file
+ENTRYPOINT ["java", "-jar", "app.jar"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 78224e8..7dbe7f0 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,47 @@
+# 모플: 매일 3문항 수학 사고력 향상 서비스
+매일 **기출 문제 3문항**을 풀며, **수학 사고력 취약점을 진단, 처방, 교정**까지 한 번에 해결하는 **올인원 수학 사고력 향상 서비스**입니다.
-
+---
-
+## 🎯 사용자 서비스 기능
+
- 빠르게 받아보는 내 취약점 복습서, 모플
- 기능1: 빠르게 틀린 문제를 체크해서 내 점수와 등급을 받아보세요.
- 기능2: 입력한 오답문제를 바탕으로 복습서를 제공합니다.
- 기능3: 친구에게 내 결과를 공유해보세요.
+- **핵심 사고 과정 점검**: 각 기출 문항은 3개의 세부 문항으로 구성되어 있어, 취약점을 명확히 진단할 수 있습니다.
+- **손해설 및 풀이 과정 제공**: 풀이 후, 문제 해결 과정에서 필요했던 핵심 사고력을 정리하여 개념 보완을 돕습니다.
+- **오답 분석 및 맞춤 처방**: 세부 문항별 오답을 분석하고, 추가 학습이 필요한 개념을 처방합니다.
+- **교정 학습 기능**: 틀린 문제와 유사한 문제를 다시 풀며, 취약한 사고 과정을 보완하고 사고력을 강화할 수 있습니다.
-
+---
- Admin 페이지
- Thymeleaf를 통해 구현한 어드민 페이지로 모의고사에 대한 정보, 정답, 정답률, 문제 이미지를 업로드할 수 있습니다.
-
-

-

-

-

+## 🛠 어드민 서비스 기능
+
+- **문항 및 세부 문항 등록**: 새 문제와 세부 문항을 손쉽게 등록할 수 있습니다.
+- **세트 구성 및 일정 관리**: 여러 문항을 세트로 묶어 원하는 날짜에 발행 가능합니다.
+- **자동 배포 기능**: 발행된 세트는 사용자에게 **"오늘의 문제"**로 제공됩니다.
-
+---
-## 서버 팀원 소개
-|[BE 개발자 박세준](https://github.com/sejoon00)|
-|:--------:|
-|

|
+## 🏗 Architecture
+
+

+
+- 서비스의 **안정적인 운영을 위해 prod(운영)과 dev(개발) 서버를 분리**하여 배포 및 관리하고 있습니다.
+
+---
+
+## 서버 팀원 소개
+|

|

|
+|:--------:|:--------:|
+|[BE 박세준](https://github.com/sejoon00)|[BE 홍석범](https://github.com/seokbeom00)|
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index bdbfc70..4409a35 100644
--- a/build.gradle
+++ b/build.gradle
@@ -23,6 +23,7 @@ repositories {
mavenCentral()
}
+
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
@@ -33,6 +34,10 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+ // Spring Security
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ testImplementation 'org.springframework.security:spring-security-test'
+
//db-h2
implementation 'com.h2database:h2'
testImplementation 'com.h2database:h2'
@@ -44,6 +49,28 @@ dependencies {
//s3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
+ // JWT
+ implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
+ runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
+ runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
+
+ // validator
+ implementation 'commons-validator:commons-validator:1.7'
+
+ // Map Struct
+ implementation 'org.mapstruct:mapstruct:1.6.3'
+ annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'
+ annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
+
+ // JPA
+ implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'
+ implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1'
+
+ // QueryDSL
+ implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
+ annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
+ annotationProcessor "jakarta.annotation:jakarta.annotation-api"
+ annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
tasks.named('test') {
diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml
index b3c514e..37eb094 100644
--- a/docker-compose-dev.yml
+++ b/docker-compose-dev.yml
@@ -22,6 +22,11 @@ services:
- CLOUD_AWS_REGION_STATIC=${AWS_REGION}
- CLOUD_AWS_REGION_AUTO=false
- CLOUD_AWS_STACK_AUTO=false
+ - JWT_ACCESS_TOKEN_SECRET=${JWT_ACCESS_TOKEN_SECRET}
+ - JWT_REFRESH_TOKEN_SECRET=${JWT_REFRESH_TOKEN_SECRET}
+ - JWT_ACCESS_TOKEN_EXPIRATION_TIME=${JWT_ACCESS_TOKEN_EXPIRATION_TIME} # 기본값: 2시간
+ - JWT_REFRESH_TOKEN_EXPIRATION_TIME=${JWT_REFRESH_TOKEN_EXPIRATION_TIME} # 기본값: 7일
+ - JWT_ISSUER=${JWT_ISSUER}
depends_on:
- mysql
diff --git a/src/main/generated/com/moplus/moplus_server/domain/concept/domain/QConceptTag.java b/src/main/generated/com/moplus/moplus_server/domain/concept/domain/QConceptTag.java
new file mode 100644
index 0000000..454956c
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/concept/domain/QConceptTag.java
@@ -0,0 +1,47 @@
+package com.moplus.moplus_server.domain.concept.domain;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QConceptTag is a Querydsl query type for ConceptTag
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QConceptTag extends EntityPathBase
{
+
+ private static final long serialVersionUID = 652954745L;
+
+ public static final QConceptTag conceptTag = new QConceptTag("conceptTag");
+
+ public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this);
+
+ //inherited
+ public final DateTimePath createdDate = _super.createdDate;
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final StringPath name = createString("name");
+
+ //inherited
+ public final DateTimePath updatedDate = _super.updatedDate;
+
+ public QConceptTag(String variable) {
+ super(ConceptTag.class, forVariable(variable));
+ }
+
+ public QConceptTag(Path extends ConceptTag> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QConceptTag(PathMetadata metadata) {
+ super(ConceptTag.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/member/domain/QMember.java b/src/main/generated/com/moplus/moplus_server/domain/member/domain/QMember.java
new file mode 100644
index 0000000..ad5aeae
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/member/domain/QMember.java
@@ -0,0 +1,53 @@
+package com.moplus.moplus_server.domain.member.domain;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QMember is a Querydsl query type for Member
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QMember extends EntityPathBase {
+
+ private static final long serialVersionUID = -705761779L;
+
+ public static final QMember member = new QMember("member1");
+
+ public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this);
+
+ //inherited
+ public final DateTimePath createdDate = _super.createdDate;
+
+ public final StringPath email = createString("email");
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final StringPath name = createString("name");
+
+ public final StringPath password = createString("password");
+
+ public final EnumPath role = createEnum("role", MemberRole.class);
+
+ //inherited
+ public final DateTimePath updatedDate = _super.updatedDate;
+
+ public QMember(String variable) {
+ super(Member.class, forVariable(variable));
+ }
+
+ public QMember(Path extends Member> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QMember(PathMetadata metadata) {
+ super(Member.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/problem/domain/QAnswer.java b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/QAnswer.java
new file mode 100644
index 0000000..cdda3fe
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/QAnswer.java
@@ -0,0 +1,37 @@
+package com.moplus.moplus_server.domain.problem.domain;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QAnswer is a Querydsl query type for Answer
+ */
+@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer")
+public class QAnswer extends BeanPath {
+
+ private static final long serialVersionUID = 983834524L;
+
+ public static final QAnswer answer = new QAnswer("answer");
+
+ public final StringPath value = createString("value");
+
+ public QAnswer(String variable) {
+ super(Answer.class, forVariable(variable));
+ }
+
+ public QAnswer(Path extends Answer> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QAnswer(PathMetadata metadata) {
+ super(Answer.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/problem/domain/childProblem/QChildProblem.java b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/childProblem/QChildProblem.java
new file mode 100644
index 0000000..193076d
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/childProblem/QChildProblem.java
@@ -0,0 +1,65 @@
+package com.moplus.moplus_server.domain.problem.domain.childProblem;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+import com.querydsl.core.types.dsl.PathInits;
+
+
+/**
+ * QChildProblem is a Querydsl query type for ChildProblem
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QChildProblem extends EntityPathBase {
+
+ private static final long serialVersionUID = 1030139824L;
+
+ private static final PathInits INITS = PathInits.DIRECT2;
+
+ public static final QChildProblem childProblem = new QChildProblem("childProblem");
+
+ public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this);
+
+ public final com.moplus.moplus_server.domain.problem.domain.QAnswer answer;
+
+ public final EnumPath answerType = createEnum("answerType", com.moplus.moplus_server.domain.problem.domain.problem.AnswerType.class);
+
+ public final SetPath> conceptTagIds = this.>createSet("conceptTagIds", Long.class, NumberPath.class, PathInits.DIRECT2);
+
+ //inherited
+ public final DateTimePath createdDate = _super.createdDate;
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final StringPath imageUrl = createString("imageUrl");
+
+ //inherited
+ public final DateTimePath updatedDate = _super.updatedDate;
+
+ public QChildProblem(String variable) {
+ this(ChildProblem.class, forVariable(variable), INITS);
+ }
+
+ public QChildProblem(Path extends ChildProblem> path) {
+ this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
+ }
+
+ public QChildProblem(PathMetadata metadata) {
+ this(metadata, PathInits.getFor(metadata, INITS));
+ }
+
+ public QChildProblem(PathMetadata metadata, PathInits inits) {
+ this(ChildProblem.class, metadata, inits);
+ }
+
+ public QChildProblem(Class extends ChildProblem> type, PathMetadata metadata, PathInits inits) {
+ super(type, metadata, inits);
+ this.answer = inits.isInitialized("answer") ? new com.moplus.moplus_server.domain.problem.domain.QAnswer(forProperty("answer")) : null;
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/problem/domain/practiceTest/QPracticeTestTag.java b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/practiceTest/QPracticeTestTag.java
new file mode 100644
index 0000000..83588ac
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/practiceTest/QPracticeTestTag.java
@@ -0,0 +1,47 @@
+package com.moplus.moplus_server.domain.problem.domain.practiceTest;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QPracticeTestTag is a Querydsl query type for PracticeTestTag
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QPracticeTestTag extends EntityPathBase {
+
+ private static final long serialVersionUID = -2120162934L;
+
+ public static final QPracticeTestTag practiceTestTag = new QPracticeTestTag("practiceTestTag");
+
+ public final StringPath area = createString("area");
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final NumberPath month = createNumber("month", Integer.class);
+
+ public final StringPath name = createString("name");
+
+ public final EnumPath subject = createEnum("subject", Subject.class);
+
+ public final NumberPath year = createNumber("year", Integer.class);
+
+ public QPracticeTestTag(String variable) {
+ super(PracticeTestTag.class, forVariable(variable));
+ }
+
+ public QPracticeTestTag(Path extends PracticeTestTag> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QPracticeTestTag(PathMetadata metadata) {
+ super(PracticeTestTag.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QDifficulty.java b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QDifficulty.java
new file mode 100644
index 0000000..ccd4d1b
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QDifficulty.java
@@ -0,0 +1,37 @@
+package com.moplus.moplus_server.domain.problem.domain.problem;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QDifficulty is a Querydsl query type for Difficulty
+ */
+@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer")
+public class QDifficulty extends BeanPath {
+
+ private static final long serialVersionUID = 175172936L;
+
+ public static final QDifficulty difficulty1 = new QDifficulty("difficulty1");
+
+ public final NumberPath difficulty = createNumber("difficulty", Integer.class);
+
+ public QDifficulty(String variable) {
+ super(Difficulty.class, forVariable(variable));
+ }
+
+ public QDifficulty(Path extends Difficulty> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QDifficulty(PathMetadata metadata) {
+ super(Difficulty.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QProblem.java b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QProblem.java
new file mode 100644
index 0000000..c06ac88
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QProblem.java
@@ -0,0 +1,99 @@
+package com.moplus.moplus_server.domain.problem.domain.problem;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+import com.querydsl.core.types.dsl.PathInits;
+
+
+/**
+ * QProblem is a Querydsl query type for Problem
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QProblem extends EntityPathBase {
+
+ private static final long serialVersionUID = -1319796686L;
+
+ private static final PathInits INITS = PathInits.DIRECT2;
+
+ public static final QProblem problem = new QProblem("problem");
+
+ public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this);
+
+ public final com.moplus.moplus_server.domain.problem.domain.QAnswer answer;
+
+ public final EnumPath answerType = createEnum("answerType", AnswerType.class);
+
+ public final ListPath childProblems = this.createList("childProblems", com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem.class, com.moplus.moplus_server.domain.problem.domain.childProblem.QChildProblem.class, PathInits.DIRECT2);
+
+ public final SetPath> conceptTagIds = this.>createSet("conceptTagIds", Long.class, NumberPath.class, PathInits.DIRECT2);
+
+ //inherited
+ public final DateTimePath createdDate = _super.createdDate;
+
+ public final QDifficulty difficulty;
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final BooleanPath isConfirmed = createBoolean("isConfirmed");
+
+ public final StringPath mainAnalysisImageUrl = createString("mainAnalysisImageUrl");
+
+ public final StringPath mainHandwritingExplanationImageUrl = createString("mainHandwritingExplanationImageUrl");
+
+ public final StringPath mainProblemImageUrl = createString("mainProblemImageUrl");
+
+ public final StringPath memo = createString("memo");
+
+ public final NumberPath number = createNumber("number", Integer.class);
+
+ public final NumberPath practiceTestId = createNumber("practiceTestId", Long.class);
+
+ public final ListPath prescriptionImageUrls = this.createList("prescriptionImageUrls", String.class, StringPath.class, PathInits.DIRECT2);
+
+ public final QProblemCustomId problemCustomId;
+
+ public final EnumPath problemType = createEnum("problemType", ProblemType.class);
+
+ public final StringPath readingTipImageUrl = createString("readingTipImageUrl");
+
+ public final QRecommendedTime recommendedTime;
+
+ public final StringPath seniorTipImageUrl = createString("seniorTipImageUrl");
+
+ public final QTitle title;
+
+ //inherited
+ public final DateTimePath updatedDate = _super.updatedDate;
+
+ public QProblem(String variable) {
+ this(Problem.class, forVariable(variable), INITS);
+ }
+
+ public QProblem(Path extends Problem> path) {
+ this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
+ }
+
+ public QProblem(PathMetadata metadata) {
+ this(metadata, PathInits.getFor(metadata, INITS));
+ }
+
+ public QProblem(PathMetadata metadata, PathInits inits) {
+ this(Problem.class, metadata, inits);
+ }
+
+ public QProblem(Class extends Problem> type, PathMetadata metadata, PathInits inits) {
+ super(type, metadata, inits);
+ this.answer = inits.isInitialized("answer") ? new com.moplus.moplus_server.domain.problem.domain.QAnswer(forProperty("answer")) : null;
+ this.difficulty = inits.isInitialized("difficulty") ? new QDifficulty(forProperty("difficulty")) : null;
+ this.problemCustomId = inits.isInitialized("problemCustomId") ? new QProblemCustomId(forProperty("problemCustomId")) : null;
+ this.recommendedTime = inits.isInitialized("recommendedTime") ? new QRecommendedTime(forProperty("recommendedTime")) : null;
+ this.title = inits.isInitialized("title") ? new QTitle(forProperty("title")) : null;
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QProblemCustomId.java b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QProblemCustomId.java
new file mode 100644
index 0000000..896d9cf
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QProblemCustomId.java
@@ -0,0 +1,37 @@
+package com.moplus.moplus_server.domain.problem.domain.problem;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QProblemCustomId is a Querydsl query type for ProblemCustomId
+ */
+@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer")
+public class QProblemCustomId extends BeanPath {
+
+ private static final long serialVersionUID = -517009730L;
+
+ public static final QProblemCustomId problemCustomId = new QProblemCustomId("problemCustomId");
+
+ public final StringPath id = createString("id");
+
+ public QProblemCustomId(String variable) {
+ super(ProblemCustomId.class, forVariable(variable));
+ }
+
+ public QProblemCustomId(Path extends ProblemCustomId> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QProblemCustomId(PathMetadata metadata) {
+ super(ProblemCustomId.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QRecommendedTime.java b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QRecommendedTime.java
new file mode 100644
index 0000000..2ef9140
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QRecommendedTime.java
@@ -0,0 +1,39 @@
+package com.moplus.moplus_server.domain.problem.domain.problem;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QRecommendedTime is a Querydsl query type for RecommendedTime
+ */
+@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer")
+public class QRecommendedTime extends BeanPath {
+
+ private static final long serialVersionUID = -1102611877L;
+
+ public static final QRecommendedTime recommendedTime = new QRecommendedTime("recommendedTime");
+
+ public final NumberPath minute = createNumber("minute", Integer.class);
+
+ public final NumberPath second = createNumber("second", Integer.class);
+
+ public QRecommendedTime(String variable) {
+ super(RecommendedTime.class, forVariable(variable));
+ }
+
+ public QRecommendedTime(Path extends RecommendedTime> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QRecommendedTime(PathMetadata metadata) {
+ super(RecommendedTime.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QTitle.java b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QTitle.java
new file mode 100644
index 0000000..4c4cdd1
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QTitle.java
@@ -0,0 +1,37 @@
+package com.moplus.moplus_server.domain.problem.domain.problem;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QTitle is a Querydsl query type for Title
+ */
+@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer")
+public class QTitle extends BeanPath {
+
+ private static final long serialVersionUID = 42281131L;
+
+ public static final QTitle title1 = new QTitle("title1");
+
+ public final StringPath title = createString("title");
+
+ public QTitle(String variable) {
+ super(Title.class, forVariable(variable));
+ }
+
+ public QTitle(Path extends Title> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QTitle(PathMetadata metadata) {
+ super(Title.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapperImpl.java b/src/main/generated/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapperImpl.java
new file mode 100644
index 0000000..e682008
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapperImpl.java
@@ -0,0 +1,57 @@
+package com.moplus.moplus_server.domain.problem.service.mapper;
+
+import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem;
+import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemPostRequest;
+import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemUpdateRequest;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import javax.annotation.processing.Generated;
+import org.springframework.stereotype.Component;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2025-02-21T03:13:14+0900",
+ comments = "version: 1.6.3, compiler: javac, environment: Java 17.0.10 (JetBrains s.r.o.)"
+)
+@Component
+public class ChildProblemMapperImpl implements ChildProblemMapper {
+
+ @Override
+ public ChildProblem from(ChildProblemPostRequest request) {
+ if ( request == null ) {
+ return null;
+ }
+
+ ChildProblem.ChildProblemBuilder childProblem = ChildProblem.builder();
+
+ childProblem.imageUrl( request.imageUrl() );
+ childProblem.answerType( request.answerType() );
+ childProblem.answer( request.answer() );
+ Set set = request.conceptTagIds();
+ if ( set != null ) {
+ childProblem.conceptTagIds( new LinkedHashSet( set ) );
+ }
+
+ return childProblem.build();
+ }
+
+ @Override
+ public ChildProblem from(ChildProblemUpdateRequest request) {
+ if ( request == null ) {
+ return null;
+ }
+
+ ChildProblem.ChildProblemBuilder childProblem = ChildProblem.builder();
+
+ childProblem.id( request.childProblemId() );
+ childProblem.imageUrl( request.imageUrl() );
+ childProblem.answerType( request.answerType() );
+ childProblem.answer( request.answer() );
+ Set set = request.conceptTagIds();
+ if ( set != null ) {
+ childProblem.conceptTagIds( new LinkedHashSet( set ) );
+ }
+
+ return childProblem.build();
+ }
+}
diff --git a/src/main/generated/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapperImpl.java b/src/main/generated/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapperImpl.java
new file mode 100644
index 0000000..4007f06
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapperImpl.java
@@ -0,0 +1,93 @@
+package com.moplus.moplus_server.domain.problem.service.mapper;
+
+import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag;
+import com.moplus.moplus_server.domain.problem.domain.problem.Problem;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemCustomId;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType;
+import com.moplus.moplus_server.domain.problem.dto.request.ProblemPostRequest;
+import com.moplus.moplus_server.domain.problem.dto.request.ProblemUpdateRequest;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.processing.Generated;
+import org.springframework.stereotype.Component;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2025-02-21T03:13:14+0900",
+ comments = "version: 1.6.3, compiler: javac, environment: Java 17.0.10 (JetBrains s.r.o.)"
+)
+@Component
+public class ProblemMapperImpl implements ProblemMapper {
+
+ @Override
+ public Problem from(ProblemPostRequest request, ProblemCustomId problemCustomId, PracticeTestTag practiceTestTag) {
+ if ( request == null && problemCustomId == null && practiceTestTag == null ) {
+ return null;
+ }
+
+ Problem.ProblemBuilder problem = Problem.builder();
+
+ if ( request != null ) {
+ problem.problemType( request.problemType() );
+ problem.number( request.number() );
+ }
+ problem.problemCustomId( problemCustomId );
+ problem.practiceTestTag( practiceTestTag );
+
+ return problem.build();
+ }
+
+ @Override
+ public Problem from(ProblemUpdateRequest request, ProblemCustomId problemCustomId, PracticeTestTag practiceTestTag) {
+ if ( request == null && problemCustomId == null && practiceTestTag == null ) {
+ return null;
+ }
+
+ Problem.ProblemBuilder problem = Problem.builder();
+
+ if ( request != null ) {
+ problem.recommendedMinute( request.recommendedMinute() );
+ problem.recommendedSecond( request.recommendedSecond() );
+ problem.answerType( request.answerType() );
+ Set set = request.conceptTagIds();
+ if ( set != null ) {
+ problem.conceptTagIds( new LinkedHashSet( set ) );
+ }
+ problem.difficulty( request.difficulty() );
+ problem.mainHandwritingExplanationImageUrl( request.mainHandwritingExplanationImageUrl() );
+ List list = request.prescriptionImageUrls();
+ if ( list != null ) {
+ problem.prescriptionImageUrls( new ArrayList( list ) );
+ }
+ problem.seniorTipImageUrl( request.seniorTipImageUrl() );
+ problem.readingTipImageUrl( request.readingTipImageUrl() );
+ problem.mainAnalysisImageUrl( request.mainAnalysisImageUrl() );
+ problem.mainProblemImageUrl( request.mainProblemImageUrl() );
+ problem.memo( request.memo() );
+ problem.answer( request.answer() );
+ problem.title( request.title() );
+ problem.problemType( request.problemType() );
+ problem.number( request.number() );
+ }
+ problem.problemCustomId( problemCustomId );
+ problem.practiceTestTag( practiceTestTag );
+
+ return problem.build();
+ }
+
+ @Override
+ public Problem from(ProblemType problemType, ProblemCustomId problemCustomId) {
+ if ( problemType == null && problemCustomId == null ) {
+ return null;
+ }
+
+ Problem.ProblemBuilder problem = Problem.builder();
+
+ problem.problemType( problemType );
+ problem.problemCustomId( problemCustomId );
+
+ return problem.build();
+ }
+}
diff --git a/src/main/generated/com/moplus/moplus_server/domain/problemset/domain/QProblemSet.java b/src/main/generated/com/moplus/moplus_server/domain/problemset/domain/QProblemSet.java
new file mode 100644
index 0000000..dc1cf58
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/problemset/domain/QProblemSet.java
@@ -0,0 +1,65 @@
+package com.moplus.moplus_server.domain.problemset.domain;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+import com.querydsl.core.types.dsl.PathInits;
+
+
+/**
+ * QProblemSet is a Querydsl query type for ProblemSet
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QProblemSet extends EntityPathBase {
+
+ private static final long serialVersionUID = -499971265L;
+
+ private static final PathInits INITS = PathInits.DIRECT2;
+
+ public static final QProblemSet problemSet = new QProblemSet("problemSet");
+
+ public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this);
+
+ public final EnumPath confirmStatus = createEnum("confirmStatus", ProblemSetConfirmStatus.class);
+
+ //inherited
+ public final DateTimePath createdDate = _super.createdDate;
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final BooleanPath isDeleted = createBoolean("isDeleted");
+
+ public final ListPath> problemIds = this.>createList("problemIds", Long.class, NumberPath.class, PathInits.DIRECT2);
+
+ public final QTitle title;
+
+ //inherited
+ public final DateTimePath updatedDate = _super.updatedDate;
+
+ public QProblemSet(String variable) {
+ this(ProblemSet.class, forVariable(variable), INITS);
+ }
+
+ public QProblemSet(Path extends ProblemSet> path) {
+ this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
+ }
+
+ public QProblemSet(PathMetadata metadata) {
+ this(metadata, PathInits.getFor(metadata, INITS));
+ }
+
+ public QProblemSet(PathMetadata metadata, PathInits inits) {
+ this(ProblemSet.class, metadata, inits);
+ }
+
+ public QProblemSet(Class extends ProblemSet> type, PathMetadata metadata, PathInits inits) {
+ super(type, metadata, inits);
+ this.title = inits.isInitialized("title") ? new QTitle(forProperty("title")) : null;
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/problemset/domain/QTitle.java b/src/main/generated/com/moplus/moplus_server/domain/problemset/domain/QTitle.java
new file mode 100644
index 0000000..d6ad4db
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/problemset/domain/QTitle.java
@@ -0,0 +1,37 @@
+package com.moplus.moplus_server.domain.problemset.domain;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QTitle is a Querydsl query type for Title
+ */
+@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer")
+public class QTitle extends BeanPath {
+
+ private static final long serialVersionUID = -324250916L;
+
+ public static final QTitle title = new QTitle("title");
+
+ public final StringPath value = createString("value");
+
+ public QTitle(String variable) {
+ super(Title.class, forVariable(variable));
+ }
+
+ public QTitle(Path extends Title> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QTitle(PathMetadata metadata) {
+ super(Title.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/publish/domain/QPublish.java b/src/main/generated/com/moplus/moplus_server/domain/publish/domain/QPublish.java
new file mode 100644
index 0000000..41676a4
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/publish/domain/QPublish.java
@@ -0,0 +1,49 @@
+package com.moplus.moplus_server.domain.publish.domain;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QPublish is a Querydsl query type for Publish
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QPublish extends EntityPathBase {
+
+ private static final long serialVersionUID = 1565569153L;
+
+ public static final QPublish publish = new QPublish("publish");
+
+ public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this);
+
+ //inherited
+ public final DateTimePath createdDate = _super.createdDate;
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final NumberPath problemSetId = createNumber("problemSetId", Long.class);
+
+ public final DatePath publishedDate = createDate("publishedDate", java.time.LocalDate.class);
+
+ //inherited
+ public final DateTimePath updatedDate = _super.updatedDate;
+
+ public QPublish(String variable) {
+ super(Publish.class, forVariable(variable));
+ }
+
+ public QPublish(Path extends Publish> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QPublish(PathMetadata metadata) {
+ super(Publish.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/v0/DetailResultApplication/entity/QDetailResultApplication.java b/src/main/generated/com/moplus/moplus_server/domain/v0/DetailResultApplication/entity/QDetailResultApplication.java
new file mode 100644
index 0000000..9ff44d0
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/v0/DetailResultApplication/entity/QDetailResultApplication.java
@@ -0,0 +1,51 @@
+package com.moplus.moplus_server.domain.v0.DetailResultApplication.entity;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QDetailResultApplication is a Querydsl query type for DetailResultApplication
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QDetailResultApplication extends EntityPathBase {
+
+ private static final long serialVersionUID = 215702330L;
+
+ public static final QDetailResultApplication detailResultApplication = new QDetailResultApplication("detailResultApplication");
+
+ public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this);
+
+ //inherited
+ public final DateTimePath createdDate = _super.createdDate;
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final StringPath name = createString("name");
+
+ public final StringPath phoneNumber = createString("phoneNumber");
+
+ public final NumberPath testResultId = createNumber("testResultId", Long.class);
+
+ //inherited
+ public final DateTimePath updatedDate = _super.updatedDate;
+
+ public QDetailResultApplication(String variable) {
+ super(DetailResultApplication.class, forVariable(variable));
+ }
+
+ public QDetailResultApplication(Path extends DetailResultApplication> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QDetailResultApplication(PathMetadata metadata) {
+ super(DetailResultApplication.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/v0/TestResult/entity/QEstimatedRating.java b/src/main/generated/com/moplus/moplus_server/domain/v0/TestResult/entity/QEstimatedRating.java
new file mode 100644
index 0000000..7dbaf22
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/v0/TestResult/entity/QEstimatedRating.java
@@ -0,0 +1,43 @@
+package com.moplus.moplus_server.domain.v0.TestResult.entity;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QEstimatedRating is a Querydsl query type for EstimatedRating
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QEstimatedRating extends EntityPathBase {
+
+ private static final long serialVersionUID = -1923088138L;
+
+ public static final QEstimatedRating estimatedRating1 = new QEstimatedRating("estimatedRating1");
+
+ public final NumberPath estimatedRating = createNumber("estimatedRating", Integer.class);
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final StringPath ratingProvider = createString("ratingProvider");
+
+ public final NumberPath testResultId = createNumber("testResultId", Long.class);
+
+ public QEstimatedRating(String variable) {
+ super(EstimatedRating.class, forVariable(variable));
+ }
+
+ public QEstimatedRating(Path extends EstimatedRating> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QEstimatedRating(PathMetadata metadata) {
+ super(EstimatedRating.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/v0/TestResult/entity/QIncorrectProblem.java b/src/main/generated/com/moplus/moplus_server/domain/v0/TestResult/entity/QIncorrectProblem.java
new file mode 100644
index 0000000..9f3b995
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/v0/TestResult/entity/QIncorrectProblem.java
@@ -0,0 +1,71 @@
+package com.moplus.moplus_server.domain.v0.TestResult.entity;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+import com.querydsl.core.types.dsl.PathInits;
+
+
+/**
+ * QIncorrectProblem is a Querydsl query type for IncorrectProblem
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QIncorrectProblem extends EntityPathBase {
+
+ private static final long serialVersionUID = 1019670237L;
+
+ private static final PathInits INITS = PathInits.DIRECT2;
+
+ public static final QIncorrectProblem incorrectProblem = new QIncorrectProblem("incorrectProblem");
+
+ public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this);
+
+ public final NumberPath correctRate = createNumber("correctRate", Double.class);
+
+ //inherited
+ public final DateTimePath createdDate = _super.createdDate;
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final StringPath incorrectAnswer = createString("incorrectAnswer");
+
+ public final NumberPath point = createNumber("point", Integer.class);
+
+ public final NumberPath practiceTestId = createNumber("practiceTestId", Long.class);
+
+ public final NumberPath problemId = createNumber("problemCustomId", Long.class);
+
+ public final StringPath problemNumber = createString("problemNumber");
+
+ public final QTestResult testResult;
+
+ //inherited
+ public final DateTimePath updatedDate = _super.updatedDate;
+
+ public QIncorrectProblem(String variable) {
+ this(IncorrectProblem.class, forVariable(variable), INITS);
+ }
+
+ public QIncorrectProblem(Path extends IncorrectProblem> path) {
+ this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
+ }
+
+ public QIncorrectProblem(PathMetadata metadata) {
+ this(metadata, PathInits.getFor(metadata, INITS));
+ }
+
+ public QIncorrectProblem(PathMetadata metadata, PathInits inits) {
+ this(IncorrectProblem.class, metadata, inits);
+ }
+
+ public QIncorrectProblem(Class extends IncorrectProblem> type, PathMetadata metadata, PathInits inits) {
+ super(type, metadata, inits);
+ this.testResult = inits.isInitialized("testResult") ? new QTestResult(forProperty("testResult")) : null;
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/v0/TestResult/entity/QTestResult.java b/src/main/generated/com/moplus/moplus_server/domain/v0/TestResult/entity/QTestResult.java
new file mode 100644
index 0000000..5f8fb8b
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/v0/TestResult/entity/QTestResult.java
@@ -0,0 +1,51 @@
+package com.moplus.moplus_server.domain.v0.TestResult.entity;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QTestResult is a Querydsl query type for TestResult
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QTestResult extends EntityPathBase {
+
+ private static final long serialVersionUID = -1200752334L;
+
+ public static final QTestResult testResult = new QTestResult("testResult");
+
+ public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this);
+
+ //inherited
+ public final DateTimePath createdDate = _super.createdDate;
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final NumberPath practiceTestId = createNumber("practiceTestId", Long.class);
+
+ public final NumberPath score = createNumber("score", Integer.class);
+
+ public final ComparablePath solvingTime = createComparable("solvingTime", java.time.Duration.class);
+
+ //inherited
+ public final DateTimePath updatedDate = _super.updatedDate;
+
+ public QTestResult(String variable) {
+ super(TestResult.class, forVariable(variable));
+ }
+
+ public QTestResult(Path extends TestResult> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QTestResult(PathMetadata metadata) {
+ super(TestResult.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/v0/practiceTest/domain/QPracticeTest.java b/src/main/generated/com/moplus/moplus_server/domain/v0/practiceTest/domain/QPracticeTest.java
new file mode 100644
index 0000000..38ac923
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/v0/practiceTest/domain/QPracticeTest.java
@@ -0,0 +1,61 @@
+package com.moplus.moplus_server.domain.v0.practiceTest.domain;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QPracticeTest is a Querydsl query type for PracticeTest
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QPracticeTest extends EntityPathBase {
+
+ private static final long serialVersionUID = -700271987L;
+
+ public static final QPracticeTest practiceTest = new QPracticeTest("practiceTest");
+
+ public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this);
+
+ public final ComparablePath averageSolvingTime = createComparable("averageSolvingTime", java.time.Duration.class);
+
+ //inherited
+ public final DateTimePath createdDate = _super.createdDate;
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final StringPath name = createString("name");
+
+ public final StringPath provider = createString("provider");
+
+ public final NumberPath publicationYear = createNumber("publicationYear", Integer.class);
+
+ public final StringPath round = createString("round");
+
+ public final NumberPath solvesCount = createNumber("solvesCount", Integer.class);
+
+ public final EnumPath subject = createEnum("subject", com.moplus.moplus_server.domain.problem.domain.practiceTest.Subject.class);
+
+ //inherited
+ public final DateTimePath updatedDate = _super.updatedDate;
+
+ public final NumberPath viewCount = createNumber("viewCount", Long.class);
+
+ public QPracticeTest(String variable) {
+ super(PracticeTest.class, forVariable(variable));
+ }
+
+ public QPracticeTest(Path extends PracticeTest> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QPracticeTest(PathMetadata metadata) {
+ super(PracticeTest.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/v0/practiceTest/domain/QProblemForTest.java b/src/main/generated/com/moplus/moplus_server/domain/v0/practiceTest/domain/QProblemForTest.java
new file mode 100644
index 0000000..031ef66
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/v0/practiceTest/domain/QProblemForTest.java
@@ -0,0 +1,82 @@
+package com.moplus.moplus_server.domain.v0.practiceTest.domain;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+import com.querydsl.core.types.dsl.PathInits;
+
+
+/**
+ * QProblemForTest is a Querydsl query type for ProblemForTest
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QProblemForTest extends EntityPathBase {
+
+ private static final long serialVersionUID = 159101820L;
+
+ private static final PathInits INITS = PathInits.DIRECT2;
+
+ public static final QProblemForTest problemForTest = new QProblemForTest("problemForTest");
+
+ public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this);
+
+ public final StringPath answer = createString("answer");
+
+ public final EnumPath answerFormat = createEnum("answerFormat", AnswerFormat.class);
+
+ public final StringPath conceptType = createString("conceptType");
+
+ public final NumberPath correctRate = createNumber("correctRate", Double.class);
+
+ //inherited
+ public final DateTimePath createdDate = _super.createdDate;
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final QProblemImageForTest image;
+
+ public final NumberPath incorrectNum = createNumber("incorrectNum", Long.class);
+
+ public final NumberPath point = createNumber("point", Integer.class);
+
+ public final QPracticeTest practiceTest;
+
+ public final StringPath problemNumber = createString("problemNumber");
+
+ public final EnumPath problemRating = createEnum("problemRating", ProblemRating.class);
+
+ public final StringPath subunit = createString("subunit");
+
+ public final StringPath unit = createString("unit");
+
+ //inherited
+ public final DateTimePath updatedDate = _super.updatedDate;
+
+ public QProblemForTest(String variable) {
+ this(ProblemForTest.class, forVariable(variable), INITS);
+ }
+
+ public QProblemForTest(Path extends ProblemForTest> path) {
+ this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
+ }
+
+ public QProblemForTest(PathMetadata metadata) {
+ this(metadata, PathInits.getFor(metadata, INITS));
+ }
+
+ public QProblemForTest(PathMetadata metadata, PathInits inits) {
+ this(ProblemForTest.class, metadata, inits);
+ }
+
+ public QProblemForTest(Class extends ProblemForTest> type, PathMetadata metadata, PathInits inits) {
+ super(type, metadata, inits);
+ this.image = inits.isInitialized("image") ? new QProblemImageForTest(forProperty("image")) : null;
+ this.practiceTest = inits.isInitialized("practiceTest") ? new QPracticeTest(forProperty("practiceTest")) : null;
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/v0/practiceTest/domain/QProblemImageForTest.java b/src/main/generated/com/moplus/moplus_server/domain/v0/practiceTest/domain/QProblemImageForTest.java
new file mode 100644
index 0000000..67b69aa
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/v0/practiceTest/domain/QProblemImageForTest.java
@@ -0,0 +1,43 @@
+package com.moplus.moplus_server.domain.v0.practiceTest.domain;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QProblemImageForTest is a Querydsl query type for ProblemImageForTest
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QProblemImageForTest extends EntityPathBase {
+
+ private static final long serialVersionUID = 1499588927L;
+
+ public static final QProblemImageForTest problemImageForTest = new QProblemImageForTest("problemImageForTest");
+
+ public final StringPath fileName = createString("fileName");
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final StringPath imageUrl = createString("imageUrl");
+
+ public final NumberPath problemId = createNumber("problemCustomId", Long.class);
+
+ public QProblemImageForTest(String variable) {
+ super(ProblemImageForTest.class, forVariable(variable));
+ }
+
+ public QProblemImageForTest(Path extends ProblemImageForTest> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QProblemImageForTest(PathMetadata metadata) {
+ super(ProblemImageForTest.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/domain/v0/practiceTest/domain/QRatingTable.java b/src/main/generated/com/moplus/moplus_server/domain/v0/practiceTest/domain/QRatingTable.java
new file mode 100644
index 0000000..842309d
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/domain/v0/practiceTest/domain/QRatingTable.java
@@ -0,0 +1,54 @@
+package com.moplus.moplus_server.domain.v0.practiceTest.domain;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+import com.querydsl.core.types.dsl.PathInits;
+
+
+/**
+ * QRatingTable is a Querydsl query type for RatingTable
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QRatingTable extends EntityPathBase {
+
+ private static final long serialVersionUID = -1985854959L;
+
+ public static final QRatingTable ratingTable = new QRatingTable("ratingTable");
+
+ public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this);
+
+ //inherited
+ public final DateTimePath createdDate = _super.createdDate;
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final NumberPath practiceTestId = createNumber("practiceTestId", Long.class);
+
+ public final StringPath ratingProvider = createString("ratingProvider");
+
+ public final ListPath> ratingRows = this.>createList("ratingRows", RatingRow.class, SimplePath.class, PathInits.DIRECT2);
+
+ public final EnumPath subject = createEnum("subject", com.moplus.moplus_server.domain.problem.domain.practiceTest.Subject.class);
+
+ //inherited
+ public final DateTimePath updatedDate = _super.updatedDate;
+
+ public QRatingTable(String variable) {
+ super(RatingTable.class, forVariable(variable));
+ }
+
+ public QRatingTable(Path extends RatingTable> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QRatingTable(PathMetadata metadata) {
+ super(RatingTable.class, metadata);
+ }
+
+}
+
diff --git a/src/main/generated/com/moplus/moplus_server/global/common/QBaseEntity.java b/src/main/generated/com/moplus/moplus_server/global/common/QBaseEntity.java
new file mode 100644
index 0000000..8c5284b
--- /dev/null
+++ b/src/main/generated/com/moplus/moplus_server/global/common/QBaseEntity.java
@@ -0,0 +1,39 @@
+package com.moplus.moplus_server.global.common;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QBaseEntity is a Querydsl query type for BaseEntity
+ */
+@Generated("com.querydsl.codegen.DefaultSupertypeSerializer")
+public class QBaseEntity extends EntityPathBase {
+
+ private static final long serialVersionUID = -1014955751L;
+
+ public static final QBaseEntity baseEntity = new QBaseEntity("baseEntity");
+
+ public final DateTimePath createdDate = createDateTime("createdDate", java.time.LocalDateTime.class);
+
+ public final DateTimePath updatedDate = createDateTime("updatedDate", java.time.LocalDateTime.class);
+
+ public QBaseEntity(String variable) {
+ super(BaseEntity.class, forVariable(variable));
+ }
+
+ public QBaseEntity(Path extends BaseEntity> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QBaseEntity(PathMetadata metadata) {
+ super(BaseEntity.class, metadata);
+ }
+
+}
+
diff --git a/src/main/java/com/moplus/moplus_server/MoplusServerApplication.java b/src/main/java/com/moplus/moplus_server/MoplusServerApplication.java
index da5ce8f..f4f7565 100644
--- a/src/main/java/com/moplus/moplus_server/MoplusServerApplication.java
+++ b/src/main/java/com/moplus/moplus_server/MoplusServerApplication.java
@@ -1,14 +1,11 @@
package com.moplus.moplus_server;
-import io.swagger.v3.oas.annotations.OpenAPIDefinition;
-import io.swagger.v3.oas.annotations.servers.Server;
import java.util.Arrays;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
-@OpenAPIDefinition(servers = {@Server(url = "https://dev.mopl.kr", description = "Default Server URL")})
@SpringBootApplication
@EnableJpaAuditing
public class MoplusServerApplication {
diff --git a/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/dto/request/DetailResultApplicationPostRequest.java b/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/dto/request/DetailResultApplicationPostRequest.java
deleted file mode 100644
index a94832e..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/dto/request/DetailResultApplicationPostRequest.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.moplus.moplus_server.domain.DetailResultApplication.dto.request;
-
-import com.moplus.moplus_server.domain.DetailResultApplication.entity.DetailResultApplication;
-
-public record DetailResultApplicationPostRequest(
- Long testResultId,
- String name,
- String phoneNumber
-) {
-
- public DetailResultApplication toEntity() {
- return DetailResultApplication.builder()
- .testResultId(testResultId)
- .name(name)
- .phoneNumber(phoneNumber)
- .build();
- }
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/dto/response/ProblemGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/dto/response/ProblemGetResponse.java
deleted file mode 100644
index 4420a72..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/dto/response/ProblemGetResponse.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.moplus.moplus_server.domain.DetailResultApplication.dto.response;
-
-import com.moplus.moplus_server.domain.practiceTest.domain.Problem;
-import com.moplus.moplus_server.domain.practiceTest.domain.ProblemImage;
-import com.moplus.moplus_server.domain.practiceTest.domain.ProblemRating;
-import lombok.Builder;
-
-@Builder
-public record ProblemGetResponse(
- String problemNumber,
- String difficultLevel,
- double correctRate,
- String rating,
- String imageUrl
-) {
-
- public static ProblemGetResponse of(
- Problem problem
- ) {
- ProblemRating problemRating = problem.getProblemRating();
- ProblemImage image = problem.getImage();
- return ProblemGetResponse.builder()
- .problemNumber(problem.getProblemNumber())
- .difficultLevel(problemRating.getDifficultyLevel())
- .correctRate(problem.getCorrectRate())
- .rating(problemRating.getRating())
- .imageUrl(image.getImageUrl())
- .build();
- }
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/respository/DetailResultApplicationRepository.java b/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/respository/DetailResultApplicationRepository.java
deleted file mode 100644
index 17d4a08..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/respository/DetailResultApplicationRepository.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.moplus.moplus_server.domain.DetailResultApplication.respository;
-
-import com.moplus.moplus_server.domain.DetailResultApplication.entity.DetailResultApplication;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface DetailResultApplicationRepository extends JpaRepository {
-
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/TestResult/dto/request/IncorrectProblemPostRequest.java b/src/main/java/com/moplus/moplus_server/domain/TestResult/dto/request/IncorrectProblemPostRequest.java
deleted file mode 100644
index 438f65a..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/TestResult/dto/request/IncorrectProblemPostRequest.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.moplus.moplus_server.domain.TestResult.dto.request;
-
-import com.moplus.moplus_server.domain.TestResult.entity.IncorrectProblem;
-import com.moplus.moplus_server.domain.practiceTest.domain.Problem;
-
-public record IncorrectProblemPostRequest(
- String problemNumber,
- String incorrectAnswer
-) {
-
- public IncorrectProblem toEntity(Problem problem){
- return IncorrectProblem.builder()
- .problemNumber(problemNumber)
- .incorrectAnswer(incorrectAnswer)
- .point(problem.getPoint())
- .problemId(problem.getId())
- .build();
- }
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/TestResult/dto/response/IncorrectProblemGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/TestResult/dto/response/IncorrectProblemGetResponse.java
deleted file mode 100644
index 153d0ea..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/TestResult/dto/response/IncorrectProblemGetResponse.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.moplus.moplus_server.domain.TestResult.dto.response;
-
-import com.moplus.moplus_server.domain.TestResult.entity.IncorrectProblem;
-import lombok.Builder;
-
-@Builder
-public record IncorrectProblemGetResponse(
- String problemNumber,
- double correctRate
-) {
-
- public static IncorrectProblemGetResponse from(IncorrectProblem incorrectProblem) {
- return IncorrectProblemGetResponse.builder()
- .problemNumber(incorrectProblem.getProblemNumber())
- .correctRate(incorrectProblem.getCorrectRate())
- .build();
- }
-
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/TestResult/service/IncorrectProblemService.java b/src/main/java/com/moplus/moplus_server/domain/TestResult/service/IncorrectProblemService.java
deleted file mode 100644
index 99c24a2..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/TestResult/service/IncorrectProblemService.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.moplus.moplus_server.domain.TestResult.service;
-
-import com.moplus.moplus_server.domain.TestResult.dto.request.IncorrectProblemPostRequest;
-import com.moplus.moplus_server.domain.TestResult.dto.response.IncorrectProblemGetResponse;
-import com.moplus.moplus_server.domain.TestResult.entity.IncorrectProblem;
-import com.moplus.moplus_server.domain.TestResult.entity.TestResult;
-import com.moplus.moplus_server.domain.TestResult.repository.IncorrectProblemRepository;
-import com.moplus.moplus_server.domain.practiceTest.domain.Problem;
-import com.moplus.moplus_server.domain.practiceTest.service.client.ProblemService;
-import java.util.ArrayList;
-import java.util.List;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Service;
-
-@Service
-@RequiredArgsConstructor
-public class IncorrectProblemService {
-
- private final IncorrectProblemRepository incorrectProblemRepository;
- private final ProblemService problemService;
-
- public List saveIncorrectProblems(
- List requests,
- Long practiceTestId,
- TestResult testResult) {
- List problems = requests.stream()
- .map(request -> problemService.getProblemByPracticeTestIdAndNumber(practiceTestId, request.problemNumber()))
- .toList();
-
- List incorrectProblems = new ArrayList<>();
- for (int i = 0; i < requests.size(); i++) {
- Problem matchedProblem = problems.get(i);
- IncorrectProblem tempIncorrectProblem = requests.get(i).toEntity(matchedProblem);
-
- tempIncorrectProblem.setTestResult(testResult);
- tempIncorrectProblem.setPracticeTestId(practiceTestId);
- tempIncorrectProblem.setCorrectRate(matchedProblem.getCorrectRate());
- IncorrectProblem save = incorrectProblemRepository.save(tempIncorrectProblem);
- incorrectProblems.add(save);
- }
- return incorrectProblems;
- }
-
- public List getResponsesByTestResultId(Long testResultId) {
- return incorrectProblemRepository.findAllByTestResultId(testResultId).stream()
- .map(IncorrectProblemGetResponse::from)
- .toList();
- }
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/auth/controller/AuthController.java b/src/main/java/com/moplus/moplus_server/domain/auth/controller/AuthController.java
new file mode 100644
index 0000000..862b52a
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/auth/controller/AuthController.java
@@ -0,0 +1,63 @@
+package com.moplus.moplus_server.domain.auth.controller;
+
+import com.moplus.moplus_server.domain.auth.dto.request.AdminLoginRequest;
+import com.moplus.moplus_server.domain.auth.dto.response.AccessTokenResponse;
+import com.moplus.moplus_server.domain.auth.dto.response.TokenResponse;
+import com.moplus.moplus_server.domain.auth.service.AuthService;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.NotFoundException;
+import com.moplus.moplus_server.global.security.utils.CookieUtil;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import java.util.Arrays;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+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.RestController;
+
+@RestController
+@RequestMapping("/api/v1/auth")
+@RequiredArgsConstructor
+public class AuthController implements AuthControllerDocs {
+
+ private final AuthService authService;
+ private final CookieUtil cookieUtil;
+
+ private static String validateRefreshTokenCookie(HttpServletRequest request) {
+ if (request.getCookies() == null) {
+ throw new NotFoundException(ErrorCode.BLANK_INPUT_VALUE);
+ }
+ Cookie[] cookies = request.getCookies();
+ return Arrays.stream(cookies)
+ .filter(cookie -> "refreshToken".equals(cookie.getName()))
+ .map(Cookie::getValue)
+ .findFirst()
+ .orElseThrow(() -> new NotFoundException(ErrorCode.BLANK_INPUT_VALUE));
+ }
+
+ @Override
+ @PostMapping("/admin/login")
+ public ResponseEntity adminLogin(
+ @Valid @RequestBody AdminLoginRequest request
+ ) {
+ // 실제 처리는 Security 필터에서 이루어지며, 이 메서드는 Swagger 명세용입니다.
+ return null;
+ }
+
+ @Override
+ @GetMapping("/reissue")
+ public ResponseEntity reissueToken(HttpServletRequest request, HttpServletResponse response) {
+ String refreshToken = validateRefreshTokenCookie(request);
+
+ TokenResponse tokenResponse = authService.reissueToken(refreshToken);
+
+ response.addCookie(cookieUtil.createCookie(tokenResponse.refreshToken()));
+
+ return ResponseEntity.ok(new AccessTokenResponse(tokenResponse.accessToken()));
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/auth/controller/AuthControllerDocs.java b/src/main/java/com/moplus/moplus_server/domain/auth/controller/AuthControllerDocs.java
new file mode 100644
index 0000000..90a2610
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/auth/controller/AuthControllerDocs.java
@@ -0,0 +1,81 @@
+package com.moplus.moplus_server.domain.auth.controller;
+
+import com.moplus.moplus_server.domain.auth.dto.request.AdminLoginRequest;
+import com.moplus.moplus_server.domain.auth.dto.response.AccessTokenResponse;
+import com.moplus.moplus_server.global.error.ErrorResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.headers.Header;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.ResponseEntity;
+
+@Tag(name = "인증", description = "인증 관련 API")
+public interface AuthControllerDocs {
+
+ @Operation(
+ summary = "어드민 로그인",
+ description = "이메일과 비밀번호로 로그인하여 액세스 토큰을 발급받고 리프레시 토큰을 쿠키에 설정합니다.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "로그인 성공",
+ content = @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = AccessTokenResponse.class)
+ ),
+ headers = @Header(
+ name = "Set-Cookie",
+ description = "리프레시 토큰이 담긴 HTTP Only 쿠키",
+ schema = @Schema(
+ type = "string",
+ example = "refreshToken=xxx; Path=/; HttpOnly; Secure; SameSite=None"
+ )
+ )
+ ),
+ @ApiResponse(
+ responseCode = "401",
+ description = "인증 실패 (잘못된 이메일 또는 비밀번호)",
+ content = @Content(schema = @Schema(implementation = ErrorResponse.class))
+ )
+ }
+ )
+ ResponseEntity adminLogin(AdminLoginRequest request);
+
+ @Operation(
+ summary = "토큰 재발급",
+ description = "리프레시 토큰을 통해 새로운 액세스 토큰을 발급하고 새로운 리프레시 토큰을 쿠키에 설정합니다.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "토큰 재발급 성공",
+ content = @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = AccessTokenResponse.class)
+ ),
+ headers = @Header(
+ name = "Set-Cookie",
+ description = "새로운 리프레시 토큰이 담긴 HTTP Only 쿠키",
+ schema = @Schema(
+ type = "string",
+ example = "refreshToken=xxx; Path=/; HttpOnly; Secure; SameSite=None"
+ )
+ )
+ ),
+ @ApiResponse(
+ responseCode = "401",
+ description = "유효하지 않은 리프레시 토큰",
+ content = @Content(schema = @Schema(implementation = ErrorResponse.class))
+ ),
+ @ApiResponse(
+ responseCode = "404",
+ description = "리프레시 토큰 쿠키 없음",
+ content = @Content(schema = @Schema(implementation = ErrorResponse.class))
+ )
+ }
+ )
+ ResponseEntity reissueToken(HttpServletRequest request, HttpServletResponse response);
+}
\ No newline at end of file
diff --git a/src/main/java/com/moplus/moplus_server/domain/auth/dto/request/AdminLoginRequest.java b/src/main/java/com/moplus/moplus_server/domain/auth/dto/request/AdminLoginRequest.java
new file mode 100644
index 0000000..e0c6c14
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/auth/dto/request/AdminLoginRequest.java
@@ -0,0 +1,11 @@
+package com.moplus.moplus_server.domain.auth.dto.request;
+
+import jakarta.validation.constraints.NotNull;
+
+public record AdminLoginRequest(
+ @NotNull(message = "이메일을 입력해주세요.")
+ String email,
+ @NotNull(message = "비밀번호를 입력해주세요.")
+ String password
+) {
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/auth/dto/response/AccessTokenResponse.java b/src/main/java/com/moplus/moplus_server/domain/auth/dto/response/AccessTokenResponse.java
new file mode 100644
index 0000000..25f5b8f
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/auth/dto/response/AccessTokenResponse.java
@@ -0,0 +1,9 @@
+package com.moplus.moplus_server.domain.auth.dto.response;
+
+import jakarta.validation.constraints.NotNull;
+
+public record AccessTokenResponse(
+ @NotNull(message = "accessToken을 입력해주세요.")
+ String accessToken
+) {
+}
\ No newline at end of file
diff --git a/src/main/java/com/moplus/moplus_server/domain/auth/dto/response/TokenResponse.java b/src/main/java/com/moplus/moplus_server/domain/auth/dto/response/TokenResponse.java
new file mode 100644
index 0000000..a2be86d
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/auth/dto/response/TokenResponse.java
@@ -0,0 +1,7 @@
+package com.moplus.moplus_server.domain.auth.dto.response;
+
+public record TokenResponse(
+ String accessToken,
+ String refreshToken
+) {
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/auth/service/AuthService.java b/src/main/java/com/moplus/moplus_server/domain/auth/service/AuthService.java
new file mode 100644
index 0000000..b11bac9
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/auth/service/AuthService.java
@@ -0,0 +1,65 @@
+package com.moplus.moplus_server.domain.auth.service;
+
+import com.moplus.moplus_server.domain.auth.dto.response.TokenResponse;
+import com.moplus.moplus_server.domain.member.domain.Member;
+import com.moplus.moplus_server.domain.member.service.MemberService;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.InvalidValueException;
+import com.moplus.moplus_server.global.security.exception.JwtInvalidException;
+import com.moplus.moplus_server.global.security.utils.JwtUtil;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.MalformedJwtException;
+import io.jsonwebtoken.security.SignatureException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class AuthService {
+
+ private final JwtUtil jwtUtil;
+ private final MemberService memberService;
+
+ @Transactional
+ public TokenResponse reissueToken(String refreshToken) {
+ if (refreshToken == null) {
+ throw new InvalidValueException(ErrorCode.INVALID_INPUT_VALUE);
+ }
+
+ Claims claims = getClaims(refreshToken);
+ final Member member = getMemberById(claims.getSubject());
+
+ // 새로운 액세스 토큰과 리프레시 토큰 생성
+ String newAccessToken = jwtUtil.generateAccessToken(member);
+ String newRefreshToken = jwtUtil.generateRefreshToken(member);
+
+ return new TokenResponse(newAccessToken, newRefreshToken);
+ }
+
+ private Claims getClaims(String refreshToken) {
+ Claims claims;
+ try {
+ claims = jwtUtil.getRefreshTokenClaims(refreshToken);
+ } catch (ExpiredJwtException expiredJwtException) {
+ throw new JwtInvalidException(ErrorCode.EXPIRED_TOKEN.getMessage());
+ } catch (SignatureException signatureException) {
+ throw new JwtInvalidException(ErrorCode.WRONG_TYPE_TOKEN.getMessage());
+ } catch (MalformedJwtException malformedJwtException) {
+ throw new JwtInvalidException(ErrorCode.UNSUPPORTED_TOKEN.getMessage());
+ } catch (IllegalArgumentException illegalArgumentException) {
+ throw new JwtInvalidException(ErrorCode.UNKNOWN_ERROR.getMessage());
+ }
+ return claims;
+ }
+
+ private Member getMemberById(String id) {
+ try {
+ return memberService.getMemberById(Long.parseLong(id));
+ } catch (Exception e) {
+ throw new BadCredentialsException(ErrorCode.BAD_CREDENTIALS.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/moplus/moplus_server/domain/concept/controller/ConceptTagController.java b/src/main/java/com/moplus/moplus_server/domain/concept/controller/ConceptTagController.java
new file mode 100644
index 0000000..9670ed7
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/concept/controller/ConceptTagController.java
@@ -0,0 +1,31 @@
+package com.moplus.moplus_server.domain.concept.controller;
+
+import com.moplus.moplus_server.domain.concept.dto.response.ConceptTagResponse;
+import com.moplus.moplus_server.domain.concept.repository.ConceptTagRepository;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "문항", description = "문항 관련 API")
+@RestController
+@RequestMapping("/api/v1/conceptTags")
+@RequiredArgsConstructor
+public class ConceptTagController {
+
+ private final ConceptTagRepository conceptTagRepository;
+
+ @GetMapping("")
+ @Operation(summary = "모든 개념 태그 리스트 조회")
+ public ResponseEntity> getConceptTags(
+ ) {
+ List responses = conceptTagRepository.findAll().stream()
+ .map(ConceptTagResponse::of)
+ .toList();
+ return ResponseEntity.ok(responses);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/concept/domain/ConceptTag.java b/src/main/java/com/moplus/moplus_server/domain/concept/domain/ConceptTag.java
new file mode 100644
index 0000000..90d4c83
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/concept/domain/ConceptTag.java
@@ -0,0 +1,28 @@
+package com.moplus.moplus_server.domain.concept.domain;
+
+import com.moplus.moplus_server.global.common.BaseEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class ConceptTag extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "concept_tag_id")
+ Long id;
+
+ String name;
+
+ public ConceptTag(String name) {
+ this.name = name;
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/concept/dto/response/ConceptTagResponse.java b/src/main/java/com/moplus/moplus_server/domain/concept/dto/response/ConceptTagResponse.java
new file mode 100644
index 0000000..0f5e5cd
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/concept/dto/response/ConceptTagResponse.java
@@ -0,0 +1,18 @@
+package com.moplus.moplus_server.domain.concept.dto.response;
+
+import com.moplus.moplus_server.domain.concept.domain.ConceptTag;
+import jakarta.validation.constraints.NotNull;
+
+public record ConceptTagResponse(
+ @NotNull(message = "개념 태그 ID는 필수입니다")
+ Long id,
+ @NotNull(message = "개념 태그 이름은 필수입니다")
+ String name
+) {
+ public static ConceptTagResponse of(ConceptTag entity) {
+ return new ConceptTagResponse(
+ entity.getId(),
+ entity.getName()
+ );
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/concept/repository/ConceptTagRepository.java b/src/main/java/com/moplus/moplus_server/domain/concept/repository/ConceptTagRepository.java
new file mode 100644
index 0000000..66f71a3
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/concept/repository/ConceptTagRepository.java
@@ -0,0 +1,30 @@
+package com.moplus.moplus_server.domain.concept.repository;
+
+import com.moplus.moplus_server.domain.concept.domain.ConceptTag;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.NotFoundException;
+import java.util.List;
+import java.util.Set;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ConceptTagRepository extends JpaRepository {
+
+ default void existsByIdElseThrow(Set ids) {
+ List foundIds = findAllById(ids).stream()
+ .map(ConceptTag::getId) // 엔티티의 ID 추출
+ .toList();
+
+ if (ids.size() != foundIds.size()) {
+ throw new NotFoundException(ErrorCode.CONCEPT_TAG_NOT_FOUND_IN_LIST);
+ }
+ }
+
+
+ default List findAllByIdsElseThrow(Set ids) {
+ List conceptTags = findAllById(ids);
+ if (conceptTags.size() != ids.size()) {
+ throw new NotFoundException(ErrorCode.CONCEPT_TAG_NOT_FOUND_IN_LIST);
+ }
+ return conceptTags;
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/member/controller/MemberController.java b/src/main/java/com/moplus/moplus_server/domain/member/controller/MemberController.java
new file mode 100644
index 0000000..43a0cb2
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/member/controller/MemberController.java
@@ -0,0 +1,27 @@
+package com.moplus.moplus_server.domain.member.controller;
+
+import com.moplus.moplus_server.domain.member.domain.Member;
+import com.moplus.moplus_server.domain.member.dto.response.MemberGetResponse;
+import com.moplus.moplus_server.global.annotation.AuthUser;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "회원", description = "회원 관련 API")
+@RestController
+@RequestMapping("/api/v1/member")
+@RequiredArgsConstructor
+public class MemberController {
+
+ @GetMapping("me")
+ @Operation(summary = "내 정보 조회", description = "jwt accessToken을 통해 내 정보를 조회합니다.")
+ public ResponseEntity getMyInfo(
+ @AuthUser Member member
+ ) {
+ return ResponseEntity.ok(MemberGetResponse.of(member));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/moplus/moplus_server/domain/member/domain/Member.java b/src/main/java/com/moplus/moplus_server/domain/member/domain/Member.java
new file mode 100644
index 0000000..fb000bd
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/member/domain/Member.java
@@ -0,0 +1,44 @@
+package com.moplus.moplus_server.domain.member.domain;
+
+import com.moplus.moplus_server.global.common.BaseEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Member extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "member_id")
+ private Long id;
+
+ private String name;
+ private String email;
+ private String password;
+
+ @Enumerated(EnumType.STRING)
+ private MemberRole role;
+
+ @Builder
+ public Member(String name, String email, String password, MemberRole role) {
+ this.name = name;
+ this.email = email;
+ this.password = password;
+ this.role = role;
+ }
+
+ public boolean isMatchingPassword(String password) {
+ return this.password.equals(password);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/member/domain/MemberRole.java b/src/main/java/com/moplus/moplus_server/domain/member/domain/MemberRole.java
new file mode 100644
index 0000000..3f38166
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/member/domain/MemberRole.java
@@ -0,0 +1,21 @@
+package com.moplus.moplus_server.domain.member.domain;
+
+import java.util.Arrays;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum MemberRole {
+ USER("ROLE_USER"),
+ ADMIN("ROLE_ADMIN");
+
+ private final String value;
+
+ public static MemberRole findByKey(String value) {
+ return Arrays.stream(MemberRole.values())
+ .filter(role -> role.value.equals(value))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("No role with key: " + value));
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/member/dto/response/MemberGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/member/dto/response/MemberGetResponse.java
new file mode 100644
index 0000000..0585dac
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/member/dto/response/MemberGetResponse.java
@@ -0,0 +1,18 @@
+package com.moplus.moplus_server.domain.member.dto.response;
+
+
+import com.moplus.moplus_server.domain.member.domain.Member;
+
+public record MemberGetResponse(
+ Long id,
+ String name,
+ String email
+) {
+ public static MemberGetResponse of(Member member) {
+ return new MemberGetResponse(
+ member.getId(),
+ member.getName(),
+ member.getEmail()
+ );
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/member/repository/MemberRepository.java b/src/main/java/com/moplus/moplus_server/domain/member/repository/MemberRepository.java
new file mode 100644
index 0000000..9eb0168
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/member/repository/MemberRepository.java
@@ -0,0 +1,15 @@
+package com.moplus.moplus_server.domain.member.repository;
+
+import com.moplus.moplus_server.domain.member.domain.Member;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.NotFoundException;
+import java.util.Optional;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface MemberRepository extends JpaRepository {
+ Optional findByEmail(String email);
+
+ default Member findByEmailOrThrow(String email) {
+ return findByEmail(email).orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_NOT_FOUND));
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/member/service/MemberService.java b/src/main/java/com/moplus/moplus_server/domain/member/service/MemberService.java
new file mode 100644
index 0000000..0940f76
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/member/service/MemberService.java
@@ -0,0 +1,24 @@
+package com.moplus.moplus_server.domain.member.service;
+
+import com.moplus.moplus_server.domain.member.domain.Member;
+import com.moplus.moplus_server.domain.member.repository.MemberRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class MemberService {
+
+ private final MemberRepository memberRepository;
+
+ @Transactional(readOnly = true)
+ public Member getMemberByEmail(String email) {
+ return memberRepository.findByEmailOrThrow(email);
+ }
+
+ @Transactional(readOnly = true)
+ public Member getMemberById(Long id) {
+ return memberRepository.findById(id).orElseThrow();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/practiceTest/domain/Subject.java b/src/main/java/com/moplus/moplus_server/domain/practiceTest/domain/Subject.java
deleted file mode 100644
index ff6bdfa..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/practiceTest/domain/Subject.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.moplus.moplus_server.domain.practiceTest.domain;
-
-
-import java.util.Arrays;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-@Getter
-@AllArgsConstructor
-public enum Subject {
-
-// 화법과작문("화법과작문", 45, 100),
-// 언어와매체("언어와매체", 45, 100),
- 미적분("미적분", 30, 100),
- 확률과통계("확률과통계",30, 100),
- 기하("기하",30, 100);
-// 영어("영어",45, 100),
-// 물리I("물리I",20, 50),
-// 화학I("화학I",20, 50),
-// 생명과학I("생명과학I",20, 50),
-// 지구과학I("지구과학I",20, 50),
-// 물리II("물리II",20, 50),
-// 화학II("화학II",20, 50),
-// 생명과학II("생명과학II",20, 50),
-// 지구과학II("지구과학II",20, 50),
-// 한국지리("한국지리",20, 50),
-// 세계지리("세계지리",20, 50),
-// 동아시아사("동아시아사",20, 50),
-// 생활과윤리("생활과윤리",20, 50),
-// 윤리와사상("윤리와사상",20, 50),
-// 사회문화("사회문화",20, 50),
-// 정치와법("정치와법",20, 50),
-// 경제("경제",20, 50);
-
- private final String value;
- private final int problemCount;
- private final int perfectScore;
-
- public static Subject fromValue(String value) {
- return Arrays.stream(Subject.values())
- .filter(subject -> subject.value.equals(value))
- .findFirst()
- .orElseThrow(() -> new IllegalArgumentException("해당 값에 맞는 Subject가 없습니다: " + value));
- }
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/admin/request/ProblemImageRequest.java b/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/admin/request/ProblemImageRequest.java
deleted file mode 100644
index ef572e7..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/admin/request/ProblemImageRequest.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.moplus.moplus_server.domain.practiceTest.dto.admin.request;
-
-import com.moplus.moplus_server.domain.practiceTest.domain.Problem;
-import lombok.Builder;
-
-@Builder
-public record ProblemImageRequest(
- Long problemId,
- String problemNumber,
- String imageUrl
-) {
-
- public static ProblemImageRequest of(Problem problem) {
- return ProblemImageRequest.builder()
- .problemId(problem.getId())
- .problemNumber(problem.getProblemNumber())
- .imageUrl(problem.getImage() != null ? problem.getImage().getImageUrl() : null)
- .build();
- }
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/admin/request/ProblemPostRequest.java b/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/admin/request/ProblemPostRequest.java
deleted file mode 100644
index 62026da..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/admin/request/ProblemPostRequest.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.moplus.moplus_server.domain.practiceTest.dto.admin.request;
-
-public record ProblemPostRequest(
-
-) {
-
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/admin/response/PracticeTestAdminResponse.java b/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/admin/response/PracticeTestAdminResponse.java
deleted file mode 100644
index d464fa1..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/admin/response/PracticeTestAdminResponse.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.moplus.moplus_server.domain.practiceTest.dto.admin.response;
-
-import com.moplus.moplus_server.domain.practiceTest.domain.PracticeTest;
-import lombok.Builder;
-
-@Builder
-public record PracticeTestAdminResponse(
- Long id,
- String name,
- String round,
- String provider,
- String subject
-) {
- public static PracticeTestAdminResponse from(PracticeTest practiceTest) {
- return PracticeTestAdminResponse.builder()
- .id(practiceTest.getId())
- .name(practiceTest.getName())
- .provider(practiceTest.getProvider())
- .round(practiceTest.getRound())
- .subject(practiceTest.getSubject().getValue())
- .build();
- }
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/client/response/PracticeTestGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/client/response/PracticeTestGetResponse.java
deleted file mode 100644
index 1dc3d92..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/client/response/PracticeTestGetResponse.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.moplus.moplus_server.domain.practiceTest.dto.client.response;
-
-import com.moplus.moplus_server.domain.practiceTest.domain.PracticeTest;
-import lombok.Builder;
-
-@Builder
-public record PracticeTestGetResponse(
- Long id,
- String name,
- String round,
- String provider,
- String subject,
- long viewCount,
- int totalSolvesCount
-) {
- public static PracticeTestGetResponse from(PracticeTest practiceTest) {
- return PracticeTestGetResponse.builder()
- .id(practiceTest.getId())
- .name(practiceTest.getName())
- .provider(practiceTest.getProvider())
- .round(practiceTest.getRound())
- .subject(practiceTest.getSubject().getValue())
- .viewCount(practiceTest.getViewCount())
- .totalSolvesCount(practiceTest.getSolvesCount())
- .build();
- }
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/client/response/ProblemGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/client/response/ProblemGetResponse.java
deleted file mode 100644
index 3803c3f..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/practiceTest/dto/client/response/ProblemGetResponse.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.moplus.moplus_server.domain.practiceTest.dto.client.response;
-
-import com.moplus.moplus_server.domain.practiceTest.domain.Problem;
-import lombok.Builder;
-
-@Builder
-public record ProblemGetResponse(
- Long id,
- String problemNumber,
- String answerFormat,
- String answer,
- int point,
- double correctRate
-) {
-
- public static ProblemGetResponse from(Problem problem) {
- return ProblemGetResponse.builder()
- .id(problem.getId())
- .answer(problem.getAnswer())
- .problemNumber(problem.getProblemNumber())
- .answerFormat(problem.getAnswerFormat().getValue())
- .point(problem.getPoint())
- .correctRate(problem.getCorrectRate())
- .build();
- }
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/practiceTest/repository/ProblemImageRepository.java b/src/main/java/com/moplus/moplus_server/domain/practiceTest/repository/ProblemImageRepository.java
deleted file mode 100644
index 353087b..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/practiceTest/repository/ProblemImageRepository.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.moplus.moplus_server.domain.practiceTest.repository;
-
-import com.moplus.moplus_server.domain.practiceTest.domain.ProblemImage;
-import java.util.Optional;
-import javax.swing.JPanel;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface ProblemImageRepository extends JpaRepository {
-
- Optional findByProblemId(Long problemId);
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/practiceTest/repository/ProblemRepository.java b/src/main/java/com/moplus/moplus_server/domain/practiceTest/repository/ProblemRepository.java
deleted file mode 100644
index cc79a64..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/practiceTest/repository/ProblemRepository.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.moplus.moplus_server.domain.practiceTest.repository;
-
-import com.moplus.moplus_server.domain.practiceTest.domain.PracticeTest;
-import com.moplus.moplus_server.domain.practiceTest.domain.Problem;
-import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.persistence.LockModeType;
-import java.util.List;
-import java.util.Optional;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Lock;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
-
-public interface ProblemRepository extends JpaRepository {
-
- List findAllByPracticeTestId(Long id);
-
- void deleteAllByPracticeTestId(Long id);
-
- Optional findByProblemNumberAndPracticeTestId(String problemNumber, Long practiceTest_id);
-
- @Lock(LockModeType.PESSIMISTIC_WRITE)
- @Query("SELECT p FROM Problem p WHERE p.problemNumber = :problem_number AND p.practiceTest.id = :practice_test_id")
- Optional findByProblemNumberAndPracticeTestIdWithPessimisticLock(@Param("problem_number") String problemNumber,@Param("practice_test_id") Long practiceTest_id);
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/practiceTest/service/client/ProblemService.java b/src/main/java/com/moplus/moplus_server/domain/practiceTest/service/client/ProblemService.java
deleted file mode 100644
index 42377e0..0000000
--- a/src/main/java/com/moplus/moplus_server/domain/practiceTest/service/client/ProblemService.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package com.moplus.moplus_server.domain.practiceTest.service.client;
-
-import com.moplus.moplus_server.domain.practiceTest.dto.admin.request.ProblemCreateRequest;
-import com.moplus.moplus_server.domain.practiceTest.dto.client.response.ProblemGetResponse;
-import com.moplus.moplus_server.domain.practiceTest.domain.PracticeTest;
-import com.moplus.moplus_server.domain.practiceTest.domain.Problem;
-import com.moplus.moplus_server.domain.practiceTest.repository.ProblemRepository;
-import com.moplus.moplus_server.global.error.exception.ErrorCode;
-import com.moplus.moplus_server.global.error.exception.NotFoundException;
-import jakarta.servlet.http.HttpServletRequest;
-import java.util.ArrayList;
-import java.util.List;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-@Service
-@RequiredArgsConstructor
-public class ProblemService {
-
- private final ProblemRepository problemRepository;
-
- @Transactional
- public void saveProblems(PracticeTest practiceTest, HttpServletRequest request) {
-
- List problems = new ArrayList<>();
-
- for (int i = 1; i <= practiceTest.getSubject().getProblemCount(); i++) {
- String problemNumber = String.valueOf(i);
- String answerFormat = request.getParameter("answerFormat_" + i);
- String answer = request.getParameter("answer_" + i);
- int point = Integer.parseInt(request.getParameter("point_" + i));
- double correctRate = Double.parseDouble(request.getParameter("correctRate_" + i));
-
- ProblemCreateRequest problem = new ProblemCreateRequest(problemNumber, answerFormat, answer, point,
- correctRate);
- problems.add(problem);
- }
- List problemsEntities = problems.stream()
- .map(problem -> problem.toEntity(practiceTest))
- .toList();
- problemsEntities
- .forEach(Problem::calculateProblemRating);
- problemRepository.saveAll(problemsEntities);
- }
-
- @Transactional
- public void updateProblems(PracticeTest practiceTest, HttpServletRequest request) {
- List problems = problemRepository.findAllByPracticeTestId(practiceTest.getId());
-
- for (int i = 1; i <= practiceTest.getSubject().getProblemCount(); i++) {
- Problem problem = problems.get(i - 1);
- problem.updateAnswer(request.getParameter("answer_" + i));
- problem.updatePoint(Integer.parseInt(request.getParameter("point_" + i)));
- problem.updateCorrectRate(Double.parseDouble(request.getParameter("correctRate_" + i)));
-
- problem.calculateProblemRating();
- problemRepository.save(problem);
- }
- }
-
-
- public Problem getProblemByPracticeTestIdAndNumber(Long practiceId, String problemNumber) {
- return problemRepository.findByProblemNumberAndPracticeTestId(problemNumber, practiceId)
- .orElseThrow(() -> new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND));
- }
-
- @Transactional
- public Problem updateCorrectRate(Long practiceTestId, String problemNumber, double correctRate) {
- Problem problem = problemRepository.findByProblemNumberAndPracticeTestIdWithPessimisticLock(problemNumber,
- practiceTestId)
- .orElseThrow(() -> new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND));
- problem.getPracticeTest();
- problem.updateCorrectRate(correctRate);
- return problemRepository.save(problem);
- }
-
- public List getProblemsByTestId(Long testId) {
- return problemRepository.findAllByPracticeTestId(testId).stream()
- .map(ProblemGetResponse::from)
- .toList();
- }
-}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/controller/ImageUploadController.java b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ImageUploadController.java
new file mode 100644
index 0000000..9afcfc1
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ImageUploadController.java
@@ -0,0 +1,40 @@
+package com.moplus.moplus_server.domain.problem.controller;
+
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemImageType;
+import com.moplus.moplus_server.domain.problem.dto.response.PresignedUrlResponse;
+import com.moplus.moplus_server.domain.problem.service.ImageUploadService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "이미지 업로드 API", description = "이미지 업로드 관련 API")
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/images")
+public class ImageUploadController {
+
+ private final ImageUploadService imageUploadService;
+
+ @Operation(summary = "이미지 업로드를 위한 presigned URL 발급")
+ @GetMapping("/problem/{problemId}/presigned-url")
+ public ResponseEntity getProblemImagePresignedUrl(
+ @PathVariable("problemId") Long problemId,
+ @RequestParam(value = "image-type") ProblemImageType imageType) {
+ String presignedUrl = imageUploadService.generateProblemImagePresignedUrl(problemId, imageType);
+ return ResponseEntity.ok(PresignedUrlResponse.of(presignedUrl));
+ }
+
+ @Operation(summary = "이미지 업로드 완료 후 URL 조회")
+ @GetMapping("/{fileName}")
+ public ResponseEntity getImageUrl(
+ @PathVariable("fileName") String fileName) {
+ String imageUrl = imageUploadService.getImageUrl(fileName);
+ return ResponseEntity.ok(imageUrl);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java b/src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java
new file mode 100644
index 0000000..ba0703c
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java
@@ -0,0 +1,31 @@
+package com.moplus.moplus_server.domain.problem.controller;
+
+import com.moplus.moplus_server.domain.problem.dto.response.PracticeTestTagResponse;
+import com.moplus.moplus_server.domain.problem.repository.PracticeTestTagRepository;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+
+@Tag(name = "문항", description = "문항 관련 API")
+@RestController
+@RequestMapping("/api/v1/practiceTestTags")
+@RequiredArgsConstructor
+public class PracticeTestTagController {
+
+ private final PracticeTestTagRepository practiceTestTagRepository;
+
+ @GetMapping("")
+ @Operation(summary = "모의고사 목록 조회")
+ public ResponseEntity> getPracticeTestTags() {
+ List responses = practiceTestTagRepository.findAll().stream()
+ .map(PracticeTestTagResponse::of)
+ .toList();
+ return ResponseEntity.ok(responses);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemController.java b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemController.java
new file mode 100644
index 0000000..9db45a7
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemController.java
@@ -0,0 +1,90 @@
+package com.moplus.moplus_server.domain.problem.controller;
+
+import com.moplus.moplus_server.domain.problem.dto.request.ProblemPostRequest;
+import com.moplus.moplus_server.domain.problem.dto.request.ProblemUpdateRequest;
+import com.moplus.moplus_server.domain.problem.dto.response.ProblemGetResponse;
+import com.moplus.moplus_server.domain.problem.dto.response.ProblemPostResponse;
+import com.moplus.moplus_server.domain.problem.service.ChildProblemService;
+import com.moplus.moplus_server.domain.problem.service.ProblemDeleteService;
+import com.moplus.moplus_server.domain.problem.service.ProblemGetService;
+import com.moplus.moplus_server.domain.problem.service.ProblemSaveService;
+import com.moplus.moplus_server.domain.problem.service.ProblemUpdateService;
+import com.moplus.moplus_server.global.response.IdResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "문항", description = "문항 관련 API")
+@RestController
+@RequestMapping("/api/v1/problems")
+@RequiredArgsConstructor
+public class ProblemController {
+
+ private final ProblemSaveService problemSaveService;
+ private final ProblemUpdateService problemUpdateService;
+ private final ProblemGetService problemGetService;
+ private final ProblemDeleteService problemDeleteService;
+ private final ChildProblemService childProblemService;
+
+ @GetMapping("/{id}")
+ @Operation(summary = "문항 조회", description = "문항를 조회합니다.")
+ public ResponseEntity getProblem(
+ @PathVariable("id") Long id
+ ) {
+ return ResponseEntity.ok(problemGetService.getProblem(id));
+ }
+
+ @PostMapping("")
+ @Operation(summary = "문항 생성", description = "문제를 생성합니다. 기출/변형 문제는 모든 값이 필수이며 창작 문제는 문항 타입만 필수 입니다.")
+ public ResponseEntity createProblem(
+ @Valid @RequestBody ProblemPostRequest request
+ ) {
+ return ResponseEntity.ok(problemSaveService.createProblem(request));
+ }
+
+ @PutMapping("/{id}")
+ @Operation(summary = "문항 업데이트", description = "문제를 업데이트합니다. 새끼문항은 들어온 list의 순서로 저장됩니다.")
+ public ResponseEntity updateProblem(
+ @PathVariable("id") Long id,
+ @RequestBody ProblemUpdateRequest request
+ ) {
+ return ResponseEntity.ok(problemUpdateService.updateProblem(id, request));
+ }
+
+ @DeleteMapping("/{id}")
+ @Operation(summary = "문항 삭제")
+ public ResponseEntity updateProblem(
+ @PathVariable("id") Long id
+ ) {
+ problemDeleteService.deleteProblem(id);
+ return ResponseEntity.ok().body(null);
+ }
+
+ @PostMapping("/{problemId}/child-problems")
+ @Operation(summary = "새끼문항 추가", description = "추가되는 새끼 문항의 id를 반환합니다. 컨펌 이후에는 새끼 문항 추가가 불가능합니다.")
+ public ResponseEntity createChildProblem(
+ @PathVariable("problemId") Long problemId
+ ) {
+ return ResponseEntity.ok(new IdResponse(childProblemService.createChildProblem(problemId)));
+ }
+
+ @DeleteMapping("/{problemId}/child-problems/{childProblemId}")
+ @Operation(summary = "새끼 문항 삭제", description = "컨펌 이후에는 새끼 문항 삭제가 불가능합니다.")
+ public ResponseEntity deleteChildProblem(
+ @PathVariable("problemId") Long problemId,
+ @PathVariable("childProblemId") Long childProblemId
+ ) {
+ childProblemService.deleteChildProblem(problemId, childProblemId);
+ return ResponseEntity.ok().body(null);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemSearchController.java b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemSearchController.java
new file mode 100644
index 0000000..1ddf542
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemSearchController.java
@@ -0,0 +1,37 @@
+package com.moplus.moplus_server.domain.problem.controller;
+
+import com.moplus.moplus_server.domain.problem.dto.response.ProblemSearchGetResponse;
+import com.moplus.moplus_server.domain.problem.repository.ProblemSearchRepositoryCustom;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "문항", description = "문항 관련 API")
+@RestController
+@RequestMapping("/api/v1/problems")
+@RequiredArgsConstructor
+public class ProblemSearchController {
+
+ private final ProblemSearchRepositoryCustom problemSearchRepository;
+
+ @GetMapping("/search")
+ @Operation(
+ summary = "문제 검색",
+ description = "문항 ID, 문제명, 개념 태그리스트로 문제를 검색합니다. 개념 태그리스트는 OR 조건으로 검색하며 값이 없으면 쿼리파라미터에서 빼주세요"
+ )
+ public ResponseEntity> search(
+ @RequestParam(value = "problemCustomId", required = false) String problemCustomId,
+ @RequestParam(value = "title", required = false) String title,
+ @RequestParam(value = "conceptTagIds", required = false) List conceptTagIds
+ ) {
+ List problems = problemSearchRepository.search(problemCustomId, title,
+ conceptTagIds);
+ return ResponseEntity.ok(problems);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/Answer.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/Answer.java
new file mode 100644
index 0000000..8f2d5aa
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/Answer.java
@@ -0,0 +1,35 @@
+package com.moplus.moplus_server.domain.problem.domain;
+
+import com.moplus.moplus_server.domain.problem.domain.problem.AnswerType;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.InvalidValueException;
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Embeddable
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Answer {
+
+ @Column(name = "answer")
+ private String value;
+
+ public Answer(String value, AnswerType answerType) {
+ if (value == null) {
+ return;
+ }
+ validateByType(value, answerType);
+ this.value = value;
+ }
+
+ private void validateByType(String answer, AnswerType answerType) {
+ if (answerType == AnswerType.MULTIPLE_CHOICE) {
+ if (!answer.matches("^[1-5]*$")) {
+ throw new InvalidValueException(ErrorCode.INVALID_MULTIPLE_CHOICE_ANSWER);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/childProblem/ChildProblem.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/childProblem/ChildProblem.java
new file mode 100644
index 0000000..08c83c5
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/childProblem/ChildProblem.java
@@ -0,0 +1,84 @@
+package com.moplus.moplus_server.domain.problem.domain.childProblem;
+
+import com.moplus.moplus_server.domain.problem.domain.Answer;
+import com.moplus.moplus_server.domain.problem.domain.problem.AnswerType;
+import com.moplus.moplus_server.global.common.BaseEntity;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.InvalidValueException;
+import jakarta.persistence.CollectionTable;
+import jakarta.persistence.Column;
+import jakarta.persistence.ElementCollection;
+import jakarta.persistence.Embedded;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import java.util.Set;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class ChildProblem extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "child_problem_id")
+ Long id;
+ @ElementCollection
+ @CollectionTable(name = "child_problem_concept", joinColumns = @JoinColumn(name = "child_problem_id"))
+ @Column(name = "concept_tag_id")
+ Set conceptTagIds;
+ private String imageUrl;
+ @Embedded
+ private Answer answer;
+ @Enumerated(EnumType.STRING)
+ private AnswerType answerType;
+
+ @Builder
+ public ChildProblem(Long id, String imageUrl, AnswerType answerType, String answer, Set conceptTagIds) {
+ this.id = id;
+ validateAnswerByType(answer, answerType);
+ this.imageUrl = imageUrl;
+ this.answerType = answerType;
+ this.answer = new Answer(answer, answerType);
+ this.conceptTagIds = conceptTagIds;
+ }
+
+ public static ChildProblem createEmptyChildProblem() {
+ return ChildProblem.builder()
+ .imageUrl("")
+ .answerType(AnswerType.SHORT_ANSWER)
+ .answer("")
+ .conceptTagIds(Set.of())
+ .build();
+ }
+
+ public void validateAnswerByType(String answer, AnswerType answerType) {
+ if (answerType == AnswerType.MULTIPLE_CHOICE) {
+ if (!answer.matches("^[1-5]*$")) {
+ throw new InvalidValueException(ErrorCode.INVALID_MULTIPLE_CHOICE_ANSWER);
+ }
+ }
+ }
+
+ public void update(ChildProblem input) {
+ if (!this.id.equals(input.id)) {
+ throw new InvalidValueException(ErrorCode.INVALID_CHILD_PROBLEM_SEQUENCE);
+ }
+ this.imageUrl = input.imageUrl;
+ this.answerType = input.answerType;
+ this.answer = input.answer;
+ this.conceptTagIds = input.conceptTagIds;
+ }
+
+ public String getAnswer() {
+ return answer.getValue();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTestTag.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTestTag.java
new file mode 100644
index 0000000..6f57f26
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTestTag.java
@@ -0,0 +1,42 @@
+package com.moplus.moplus_server.domain.problem.domain.practiceTest;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Entity
+@Table(name = "practice_test_tag")
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class PracticeTestTag {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "practice_test_tag_id")
+ private Long id;
+
+ private String name;
+ @Column(name = "test_year")
+ private int year;
+ @Column(name = "test_month")
+ private int month;
+ @Enumerated(value = EnumType.STRING)
+ private Subject subject;
+ private String area;
+
+ public PracticeTestTag(String name, int year, int month, Subject subject) {
+ this.name = name;
+ this.year = year;
+ this.month = month;
+ this.subject = subject;
+ this.area = "수학";
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/Subject.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/Subject.java
new file mode 100644
index 0000000..75678f1
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/Subject.java
@@ -0,0 +1,30 @@
+package com.moplus.moplus_server.domain.problem.domain.practiceTest;
+
+
+import java.util.Arrays;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum Subject {
+
+ 고1("고1", 30, 100, 1),
+ 고2("고2", 30, 100, 2),
+ 미적분("미적분", 30, 100, 3),
+ 기하("기하", 30, 100, 4),
+ 확률과통계("확률과통계", 30, 100, 5),
+ ;
+
+ private final String value;
+ private final int problemCount;
+ private final int perfectScore;
+ private final int code;
+
+ public static Subject fromValue(String value) {
+ return Arrays.stream(Subject.values())
+ .filter(subject -> subject.value.equals(value))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("해당 값에 맞는 Subject가 없습니다: " + value));
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/AnswerType.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/AnswerType.java
new file mode 100644
index 0000000..ac2760e
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/AnswerType.java
@@ -0,0 +1,12 @@
+package com.moplus.moplus_server.domain.problem.domain.problem;
+
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public enum AnswerType {
+ MULTIPLE_CHOICE("객관식"),
+ SHORT_ANSWER("주관식");
+
+
+ private final String name;
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Difficulty.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Difficulty.java
new file mode 100644
index 0000000..68a0949
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Difficulty.java
@@ -0,0 +1,32 @@
+package com.moplus.moplus_server.domain.problem.domain.problem;
+
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.InvalidValueException;
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Embeddable
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Difficulty {
+
+ @Column(name = "difficulty")
+ private Integer difficulty;
+
+ public Difficulty(Integer difficulty) {
+ validate(difficulty);
+ this.difficulty = difficulty;
+ }
+
+ private void validate(Integer difficulty) {
+ if (difficulty == null) {
+ return;
+ }
+ if (difficulty < 1 || difficulty > 10) {
+ throw new InvalidValueException(ErrorCode.INVALID_DIFFICULTY);
+ }
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java
new file mode 100644
index 0000000..ee6a5bc
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java
@@ -0,0 +1,197 @@
+package com.moplus.moplus_server.domain.problem.domain.problem;
+
+import com.moplus.moplus_server.domain.problem.domain.Answer;
+import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem;
+import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag;
+import com.moplus.moplus_server.domain.problem.repository.converter.StringListConverter;
+import com.moplus.moplus_server.global.common.BaseEntity;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.InvalidValueException;
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.CollectionTable;
+import jakarta.persistence.Column;
+import jakarta.persistence.Convert;
+import jakarta.persistence.ElementCollection;
+import jakarta.persistence.Embedded;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.OrderColumn;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Problem extends BaseEntity {
+
+ @Embedded
+ ProblemCustomId problemCustomId;
+ Long practiceTestId;
+ int number;
+ @Enumerated(EnumType.STRING)
+ ProblemType problemType;
+ @Embedded
+ Title title;
+ @Embedded
+ Answer answer;
+ @Embedded
+ Difficulty difficulty;
+
+ String memo;
+ String mainProblemImageUrl;
+ String mainAnalysisImageUrl;
+ String mainHandwritingExplanationImageUrl;
+ String readingTipImageUrl;
+ String seniorTipImageUrl;
+
+ @Convert(converter = StringListConverter.class)
+ @Column(columnDefinition = "TEXT")
+ List prescriptionImageUrls;
+ @ElementCollection
+ @CollectionTable(name = "problem_concept", joinColumns = @JoinColumn(name = "problem_id"))
+ @Column(name = "concept_tag_id")
+ Set conceptTagIds;
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "problem_id")
+ private Long id;
+
+ @Enumerated(EnumType.STRING)
+ private AnswerType answerType;
+
+ private boolean isConfirmed;
+
+ @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
+ @JoinColumn(name = "problem_id")
+ @OrderColumn(name = "sequence")
+ private List childProblems = new ArrayList<>();
+
+ @Embedded
+ private RecommendedTime recommendedTime;
+
+ @Builder
+ public Problem(List childProblems, boolean isConfirmed, AnswerType answerType,
+ Set conceptTagIds, Integer difficulty, String mainHandwritingExplanationImageUrl,
+ List prescriptionImageUrls, String seniorTipImageUrl, String readingTipImageUrl,
+ String mainAnalysisImageUrl, String mainProblemImageUrl, String memo, String answer, String title,
+ ProblemType problemType, int number, PracticeTestTag practiceTestTag,
+ ProblemCustomId problemCustomId, Integer recommendedMinute, Integer recommendedSecond) {
+ this.childProblems = childProblems;
+ this.isConfirmed = isConfirmed;
+ this.answerType = answerType;
+ this.conceptTagIds = conceptTagIds;
+ this.mainHandwritingExplanationImageUrl = mainHandwritingExplanationImageUrl;
+ this.prescriptionImageUrls = prescriptionImageUrls;
+ this.seniorTipImageUrl = seniorTipImageUrl;
+ this.readingTipImageUrl = readingTipImageUrl;
+ this.mainAnalysisImageUrl = mainAnalysisImageUrl;
+ this.mainProblemImageUrl = mainProblemImageUrl;
+ this.difficulty = new Difficulty(difficulty);
+ this.memo = memo;
+ this.answer = new Answer(answer, this.answerType);
+ this.title = new Title(title);
+ this.problemType = problemType;
+ this.number = number;
+ this.practiceTestId = practiceTestTag != null ? practiceTestTag.getId() : null;
+ this.problemCustomId = problemCustomId;
+ this.recommendedTime = new RecommendedTime(recommendedMinute, recommendedSecond);
+ }
+
+ public String getAnswer() {
+ return this.answer != null ? answer.getValue() : null;
+ }
+
+ public void update(Problem inputProblem) {
+ this.problemCustomId = new ProblemCustomId(inputProblem.getProblemCustomId());
+ this.practiceTestId = inputProblem.getPracticeTestId();
+ this.number = inputProblem.getNumber();
+ this.problemType = inputProblem.getProblemType();
+ this.title = new Title(inputProblem.getTitle());
+ this.answer = new Answer(inputProblem.getAnswer(), inputProblem.getAnswerType());
+ this.difficulty = new Difficulty(inputProblem.getDifficulty());
+ this.conceptTagIds = new HashSet<>(inputProblem.getConceptTagIds());
+ this.memo = inputProblem.getMemo();
+ this.mainProblemImageUrl = inputProblem.getMainProblemImageUrl();
+ this.mainAnalysisImageUrl = inputProblem.getMainAnalysisImageUrl();
+ this.mainHandwritingExplanationImageUrl = inputProblem.getMainHandwritingExplanationImageUrl();
+ this.readingTipImageUrl = inputProblem.getReadingTipImageUrl();
+ this.seniorTipImageUrl = inputProblem.getSeniorTipImageUrl();
+ this.prescriptionImageUrls = inputProblem.getPrescriptionImageUrls();
+ this.answerType = inputProblem.getAnswerType();
+ this.recommendedTime = new RecommendedTime(
+ inputProblem.getRecommendedTime().getMinute(),
+ inputProblem.getRecommendedTime().getSecond()
+ );
+ }
+
+ public void updateChildProblem(List inputChildProblems) {
+ if (this.childProblems.size() != inputChildProblems.size()) {
+ throw new InvalidValueException(ErrorCode.INVALID_CHILD_PROBLEM_SIZE);
+ }
+
+ for (int i = 0; i < inputChildProblems.size(); i++) {
+ this.childProblems.get(i).update(inputChildProblems.get(i));
+ }
+ }
+
+ public void addChildProblem(ChildProblem childProblem) {
+ if (this.isConfirmed) {
+ throw new InvalidValueException(ErrorCode.INVALID_CHILD_PROBLEM_SIZE);
+ }
+ this.childProblems.add(childProblem);
+ }
+
+ public void deleteChildProblem(Long childProblemId) {
+ if (this.isConfirmed) {
+ throw new InvalidValueException(ErrorCode.INVALID_CHILD_PROBLEM_SIZE);
+ }
+ this.childProblems.removeIf(childProblem -> childProblem.getId().equals(childProblemId));
+ }
+
+ public boolean isValid() {
+ return problemCustomId != null
+ && practiceTestId != null
+ && problemType != null
+ && title != null && !title.getTitle().isEmpty()
+ && answer != null && !answer.getValue().isEmpty()
+ && difficulty != null && difficulty.getDifficulty() != null
+ && memo != null && !memo.isEmpty()
+ && mainProblemImageUrl != null && !mainProblemImageUrl.isEmpty()
+ && mainAnalysisImageUrl != null && !mainAnalysisImageUrl.isEmpty()
+ && mainHandwritingExplanationImageUrl != null && !mainHandwritingExplanationImageUrl.isEmpty()
+ && readingTipImageUrl != null && !readingTipImageUrl.isEmpty()
+ && seniorTipImageUrl != null && !seniorTipImageUrl.isEmpty()
+ && prescriptionImageUrls != null && !prescriptionImageUrls.isEmpty()
+ && prescriptionImageUrls.stream().allMatch(url -> url != null && !url.isEmpty())
+ && answerType != null
+ && conceptTagIds != null && !conceptTagIds.isEmpty()
+ && recommendedTime != null;
+ }
+
+ public String getTitle() {
+ return title != null ? title.getTitle() : "제목 없음";
+ }
+
+ public Integer getDifficulty() {
+ return difficulty != null ? difficulty.getDifficulty() : null;
+ }
+
+ public String getProblemCustomId() {
+ return problemCustomId.getId();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemAdminIdService.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemAdminIdService.java
new file mode 100644
index 0000000..d779579
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemAdminIdService.java
@@ -0,0 +1,61 @@
+package com.moplus.moplus_server.domain.problem.domain.problem;
+
+import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag;
+import com.moplus.moplus_server.domain.problem.repository.ProblemRepository;
+import java.util.concurrent.atomic.AtomicInteger;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class ProblemAdminIdService {
+
+ private static final AtomicInteger SEQUENCE = new AtomicInteger(1); // XXX 값 증가를 위한 카운터
+ private final ProblemRepository problemRepository;
+
+ /*
+ 문제 ID 생성 로직
+ C : 문제 타입 ( 1: 기출문제, 2: 변형문제, 3: 창작문제 )
+ S : ( 1: 고1, 2: 고2, 3: 미적분, 4: 기하, 5: 확률과 통계, 6: 가형, 7: 나형 )
+ YY: 년도 (두 자리)
+ MM: 월 (두 자리)
+ NN : 번호 (01~99)
+ XX : 2자리 sequence 숫자
+ */
+ public ProblemCustomId nextId(int number, PracticeTestTag practiceTestTag, ProblemType problemType) {
+
+ int problemTypeCode = problemType.getCode(); // C (문제 타입)
+ int subject = practiceTestTag.getSubject().getCode(); // S (과목)
+ int year = practiceTestTag.getYear() % 100; // YY (두 자리 연도)
+ int month = practiceTestTag.getMonth(); // MM (두 자리 월)
+
+ String generatedId;
+ int sequence;
+
+ // 중복되지 않는 ID 찾을 때까지 반복
+ do {
+ sequence = SEQUENCE.getAndIncrement() % 100; // 000~999 순환
+ generatedId = String.format("%d%d%02d%02d%02d%02d",
+ problemTypeCode, subject, year, month, number, sequence);
+ } while (problemRepository.existsByProblemCustomId(new ProblemCustomId(generatedId))); // ID가 이미 존재하면 재생성
+
+ return new ProblemCustomId(generatedId);
+ }
+
+ public ProblemCustomId nextId(ProblemType problemType) {
+
+ int problemTypeCode = problemType.getCode(); // C (문제 타입)
+
+ String generatedId;
+ int sequence;
+
+ // 중복되지 않는 ID 찾을 때까지 반복
+ do {
+ sequence = SEQUENCE.getAndIncrement() % 100; // 000~999 순환
+ generatedId = String.format("%d%09d",
+ problemTypeCode, sequence);
+ } while (problemRepository.existsByProblemCustomId(new ProblemCustomId(generatedId))); // ID가 이미 존재하면 재생성
+
+ return new ProblemCustomId(generatedId);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemCustomId.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemCustomId.java
new file mode 100644
index 0000000..85648be
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemCustomId.java
@@ -0,0 +1,21 @@
+package com.moplus.moplus_server.domain.problem.domain.problem;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import java.io.Serializable;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Embeddable
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class ProblemCustomId implements Serializable {
+
+ @Column(name = "problem_custom_id")
+ private String id;
+
+ public ProblemCustomId(String id) {
+ this.id = id;
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemImageType.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemImageType.java
new file mode 100644
index 0000000..305bb72
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemImageType.java
@@ -0,0 +1,19 @@
+package com.moplus.moplus_server.domain.problem.domain.problem;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public enum ProblemImageType {
+ MAIN_PROBLEM("main-problem", "문제 이미지"),
+ MAIN_ANALYSIS("main-analysis", "분석 이미지"),
+ MAIN_HANDWRITING_EXPLANATION("main-handwriting-explanation", "손글씨 설명 이미지"),
+ READING_TIP("reading-tip", "읽기 팁 이미지"),
+ SENIOR_TIP("senior-tip", "선배 팁 이미지"),
+ PRESCRIPTION("prescription", "처방전 이미지"),
+ CHILD_PROBLEM("child-problem", "하위 문제 이미지");
+
+ private final String type;
+ private final String description;
+}
\ No newline at end of file
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemType.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemType.java
new file mode 100644
index 0000000..e02d101
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemType.java
@@ -0,0 +1,16 @@
+package com.moplus.moplus_server.domain.problem.domain.problem;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public enum ProblemType {
+
+ GICHUL_PROBLEM("기출문제", 1),
+ VARIANT_PROBLEM("변형문제", 2),
+ CREATION_PROBLEM("창작문제", 3);
+
+ private final String name;
+ private final int code;
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/RecommendedTime.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/RecommendedTime.java
new file mode 100644
index 0000000..abe8ab8
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/RecommendedTime.java
@@ -0,0 +1,34 @@
+package com.moplus.moplus_server.domain.problem.domain.problem;
+
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.InvalidValueException;
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Embeddable
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class RecommendedTime {
+ @Column(name = "recommended_minute")
+ private Integer minute;
+ @Column(name = "recommended_second")
+ private Integer second;
+
+ public RecommendedTime(Integer minute, Integer second) {
+ validateTime(minute, second);
+ this.minute = minute != null ? minute : 0;
+ this.second = second != null ? second : 0;
+ }
+
+ private void validateTime(Integer minute, Integer second) {
+ if (minute != null && (minute < 0 || minute > 60)) {
+ throw new InvalidValueException(ErrorCode.INVALID_INPUT_VALUE);
+ }
+ if (second != null && (second < 0 || second > 60)) {
+ throw new InvalidValueException(ErrorCode.INVALID_INPUT_VALUE);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Title.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Title.java
new file mode 100644
index 0000000..2aa77dc
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Title.java
@@ -0,0 +1,26 @@
+package com.moplus.moplus_server.domain.problem.domain.problem;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Embeddable
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Title {
+
+ private static final String DEFAULT_TITLE = "제목 없음";
+
+ @Column(name = "title")
+ private String title;
+
+ public Title(String title) {
+ this.title = verifyTitle(title);
+ }
+
+ private static String verifyTitle(String title) {
+ return (title == null || title.trim().isEmpty()) ? DEFAULT_TITLE : title;
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemDeleteRequest.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemDeleteRequest.java
new file mode 100644
index 0000000..07615cb
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemDeleteRequest.java
@@ -0,0 +1,6 @@
+package com.moplus.moplus_server.domain.problem.dto.request;
+
+public record ChildProblemDeleteRequest(
+ Long childProblemId
+) {
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemPostRequest.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemPostRequest.java
new file mode 100644
index 0000000..0877351
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemPostRequest.java
@@ -0,0 +1,15 @@
+package com.moplus.moplus_server.domain.problem.dto.request;
+
+import com.moplus.moplus_server.domain.problem.domain.problem.AnswerType;
+import jakarta.validation.constraints.NotNull;
+import java.util.Set;
+
+public record ChildProblemPostRequest(
+ String imageUrl,
+ AnswerType answerType,
+ String answer,
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ Set conceptTagIds,
+ int sequence
+) {
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemUpdateRequest.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemUpdateRequest.java
new file mode 100644
index 0000000..ec78dd1
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemUpdateRequest.java
@@ -0,0 +1,15 @@
+package com.moplus.moplus_server.domain.problem.dto.request;
+
+import com.moplus.moplus_server.domain.problem.domain.problem.AnswerType;
+import jakarta.validation.constraints.NotNull;
+import java.util.Set;
+
+public record ChildProblemUpdateRequest(
+ Long childProblemId,
+ String imageUrl,
+ AnswerType answerType,
+ String answer,
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ Set conceptTagIds
+) {
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemPostRequest.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemPostRequest.java
new file mode 100644
index 0000000..94f597f
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemPostRequest.java
@@ -0,0 +1,24 @@
+package com.moplus.moplus_server.domain.problem.dto.request;
+
+import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag;
+import com.moplus.moplus_server.domain.problem.domain.problem.Problem;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemCustomId;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType;
+import jakarta.validation.constraints.NotNull;
+
+public record ProblemPostRequest(
+ @NotNull(message = "문항 유형은 필수입니다")
+ ProblemType problemType,
+ Long practiceTestId,
+ int number
+) {
+ public Problem toEntity(PracticeTestTag practiceTestTag, ProblemCustomId problemCustomId) {
+ return Problem.builder()
+ .problemCustomId(problemCustomId)
+ .practiceTestTag(practiceTestTag)
+ .number(number)
+ .title("")
+ .problemType(problemType)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemUpdateRequest.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemUpdateRequest.java
new file mode 100644
index 0000000..a5aa604
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemUpdateRequest.java
@@ -0,0 +1,32 @@
+package com.moplus.moplus_server.domain.problem.dto.request;
+
+import com.moplus.moplus_server.domain.problem.domain.problem.AnswerType;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType;
+import jakarta.validation.constraints.NotNull;
+import java.util.List;
+import java.util.Set;
+
+public record ProblemUpdateRequest(
+ @NotNull(message = "문제 유형은 필수입니다")
+ ProblemType problemType,
+ Long practiceTestId,
+ int number,
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ Set conceptTagIds,
+ String answer,
+ String title,
+ Integer difficulty,
+ String memo,
+ String mainProblemImageUrl,
+ String mainAnalysisImageUrl,
+ String mainHandwritingExplanationImageUrl,
+ String readingTipImageUrl,
+ String seniorTipImageUrl,
+ List prescriptionImageUrls,
+ AnswerType answerType,
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ List updateChildProblems,
+ Integer recommendedMinute,
+ Integer recommendedSecond
+) {
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ChildProblemGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ChildProblemGetResponse.java
new file mode 100644
index 0000000..b5fc52e
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ChildProblemGetResponse.java
@@ -0,0 +1,29 @@
+package com.moplus.moplus_server.domain.problem.dto.response;
+
+import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem;
+import com.moplus.moplus_server.domain.problem.domain.problem.AnswerType;
+import jakarta.validation.constraints.NotNull;
+import java.util.Set;
+import lombok.Builder;
+
+@Builder
+public record ChildProblemGetResponse(
+ @NotNull(message = "새끼 문항 ID는 필수입니다")
+ Long childProblemId,
+ String imageUrl,
+ AnswerType answerType,
+ String answer,
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ Set conceptTagIds
+) {
+
+ public static ChildProblemGetResponse of(ChildProblem childProblem) {
+ return ChildProblemGetResponse.builder()
+ .childProblemId(childProblem.getId())
+ .imageUrl(childProblem.getImageUrl())
+ .answerType(childProblem.getAnswerType())
+ .answer(childProblem.getAnswer())
+ .conceptTagIds(childProblem.getConceptTagIds())
+ .build();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ConceptTagSearchResponse.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ConceptTagSearchResponse.java
new file mode 100644
index 0000000..1229df6
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ConceptTagSearchResponse.java
@@ -0,0 +1,19 @@
+package com.moplus.moplus_server.domain.problem.dto.response;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+public class ConceptTagSearchResponse {
+ @NotNull(message = "개념 태그 ID는 필수입니다")
+ private Long id;
+ @NotNull(message = "개념 태그 이름은 필수입니다")
+ private String name;
+
+ public ConceptTagSearchResponse(Long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/PracticeTestTagResponse.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/PracticeTestTagResponse.java
new file mode 100644
index 0000000..3f2a344
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/PracticeTestTagResponse.java
@@ -0,0 +1,18 @@
+package com.moplus.moplus_server.domain.problem.dto.response;
+
+import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag;
+import jakarta.validation.constraints.NotNull;
+
+public record PracticeTestTagResponse(
+ @NotNull(message = "기출 모의고사 태그 ID는 필수입니다")
+ Long id,
+ @NotNull(message = "기출 모의고사 태그 이름은 필수입니다")
+ String name
+) {
+ public static PracticeTestTagResponse of(PracticeTestTag practiceTestTag) {
+ return new PracticeTestTagResponse(
+ practiceTestTag.getId(),
+ practiceTestTag.getName()
+ );
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/PresignedUrlResponse.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/PresignedUrlResponse.java
new file mode 100644
index 0000000..56a2d2e
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/PresignedUrlResponse.java
@@ -0,0 +1,14 @@
+package com.moplus.moplus_server.domain.problem.dto.response;
+
+import jakarta.validation.constraints.NotNull;
+
+public record PresignedUrlResponse(
+ @NotNull(message = "사전 서명된 URL은 필수입니다")
+ String presignedUrl
+) {
+ public static PresignedUrlResponse of(String presignedUrl) {
+ return new PresignedUrlResponse(
+ presignedUrl
+ );
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ProblemGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ProblemGetResponse.java
new file mode 100644
index 0000000..06bb301
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ProblemGetResponse.java
@@ -0,0 +1,69 @@
+package com.moplus.moplus_server.domain.problem.dto.response;
+
+import com.moplus.moplus_server.domain.problem.domain.problem.AnswerType;
+import com.moplus.moplus_server.domain.problem.domain.problem.Problem;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType;
+import jakarta.validation.constraints.NotNull;
+import java.util.List;
+import java.util.Set;
+import lombok.Builder;
+
+@Builder
+public record ProblemGetResponse(
+ @NotNull(message = "문항 ID 필수입니다")
+ Long id,
+ @NotNull(message = "문항 custom ID는 필수입니다")
+ String problemCustomId,
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ Set conceptTagIds,
+ boolean isConfirmed,
+ Long practiceTestId,
+ int number,
+ Integer difficulty,
+ String title,
+ String answer,
+ String memo,
+ ProblemType problemType,
+ AnswerType answerType,
+ String mainProblemImageUrl,
+ String mainHandwritingExplanationImageUrl,
+ String mainAnalysisImageUrl,
+ String readingTipImageUrl,
+ String seniorTipImageUrl,
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ List prescriptionImageUrls,
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ List childProblems,
+ Integer recommendedMinute,
+ Integer recommendedSecond
+) {
+
+ public static ProblemGetResponse of(Problem problem) {
+
+ return ProblemGetResponse.builder()
+ .id(problem.getId())
+ .problemCustomId(problem.getProblemCustomId())
+ .conceptTagIds(problem.getConceptTagIds())
+ .isConfirmed(problem.isConfirmed())
+ .practiceTestId(problem.getPracticeTestId())
+ .number(problem.getNumber())
+ .answer(problem.getAnswer())
+ .title(problem.getTitle())
+ .difficulty(problem.getDifficulty())
+ .memo(problem.getMemo())
+ .problemType(problem.getProblemType())
+ .answerType(problem.getAnswerType())
+ .mainProblemImageUrl(problem.getMainProblemImageUrl())
+ .mainHandwritingExplanationImageUrl(problem.getMainHandwritingExplanationImageUrl())
+ .mainAnalysisImageUrl(problem.getMainAnalysisImageUrl())
+ .readingTipImageUrl(problem.getReadingTipImageUrl())
+ .seniorTipImageUrl(problem.getSeniorTipImageUrl())
+ .prescriptionImageUrls(problem.getPrescriptionImageUrls())
+ .childProblems(problem.getChildProblems().stream()
+ .map(ChildProblemGetResponse::of)
+ .toList())
+ .recommendedMinute(problem.getRecommendedTime().getMinute())
+ .recommendedSecond(problem.getRecommendedTime().getSecond())
+ .build();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ProblemPostResponse.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ProblemPostResponse.java
new file mode 100644
index 0000000..5715c5c
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ProblemPostResponse.java
@@ -0,0 +1,15 @@
+package com.moplus.moplus_server.domain.problem.dto.response;
+
+import com.moplus.moplus_server.domain.problem.domain.problem.Problem;
+import jakarta.validation.constraints.NotNull;
+
+public record ProblemPostResponse(
+ @NotNull(message = "문항 ID는 필수입니다")
+ Long id,
+ @NotNull(message = "문항 custom ID는 필수입니다")
+ String problemCustomId
+) {
+ public static ProblemPostResponse of(Problem problem) {
+ return new ProblemPostResponse(problem.getId(), problem.getProblemCustomId());
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ProblemSearchGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ProblemSearchGetResponse.java
new file mode 100644
index 0000000..b2a47ef
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ProblemSearchGetResponse.java
@@ -0,0 +1,36 @@
+package com.moplus.moplus_server.domain.problem.dto.response;
+
+import jakarta.validation.constraints.NotNull;
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+public class ProblemSearchGetResponse {
+ @NotNull(message = "문항 ID는 필수입니다")
+ private Long problemId;
+ @NotNull(message = "문항 custom ID는 필수입니다")
+ private String problemCustomId;
+ private String problemTitle;
+ private String memo;
+ private String mainProblemImageUrl;
+ @NotNull(message = "개념 태그리스트는 필수입니다")
+ private Set tagNames;
+
+ public ProblemSearchGetResponse(Long problemId, String problemCustomId, String problemTitle, String memo,
+ String mainProblemImageUrl,
+ Set tagNames) {
+ this.problemId = problemId;
+ this.problemCustomId = problemCustomId;
+ this.problemTitle = problemTitle;
+ this.memo = memo;
+ this.mainProblemImageUrl = mainProblemImageUrl;
+ this.tagNames = tagNames.stream()
+ .map(ConceptTagSearchResponse::getName)
+ .collect(Collectors.toSet());
+ }
+}
+
+
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ChildProblemRepository.java b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ChildProblemRepository.java
new file mode 100644
index 0000000..9b28686
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ChildProblemRepository.java
@@ -0,0 +1,13 @@
+package com.moplus.moplus_server.domain.problem.repository;
+
+import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.NotFoundException;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ChildProblemRepository extends JpaRepository {
+
+ default ChildProblem findByIdElseThrow(Long childProblemId) {
+ return findById(childProblemId).orElseThrow(() -> new NotFoundException(ErrorCode.CHILD_PROBLEM_NOT_FOUND));
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/repository/PracticeTestTagRepository.java b/src/main/java/com/moplus/moplus_server/domain/problem/repository/PracticeTestTagRepository.java
new file mode 100644
index 0000000..2e02d64
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/repository/PracticeTestTagRepository.java
@@ -0,0 +1,14 @@
+package com.moplus.moplus_server.domain.problem.repository;
+
+import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.NotFoundException;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface PracticeTestTagRepository extends JpaRepository {
+
+ default PracticeTestTag findByIdElseThrow(Long id) {
+ return findById(id)
+ .orElseThrow(() -> new NotFoundException(ErrorCode.PRACTICE_TEST_NOT_FOUND));
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java
new file mode 100644
index 0000000..4529f68
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java
@@ -0,0 +1,28 @@
+package com.moplus.moplus_server.domain.problem.repository;
+
+import com.moplus.moplus_server.domain.problem.domain.problem.Problem;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemCustomId;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.NotFoundException;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ProblemRepository extends JpaRepository {
+
+ boolean existsByProblemCustomId(ProblemCustomId problemCustomId);
+
+ default void existsByIdElseThrow(Long id) {
+ if (!existsById(id)) {
+ throw new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND);
+ }
+ }
+
+ default void existsByProblemAdminIdElseThrow(ProblemCustomId problemCustomId) {
+ if (!existsByProblemCustomId(problemCustomId)) {
+ throw new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND);
+ }
+ }
+
+ default Problem findByIdElseThrow(Long id) {
+ return findById(id).orElseThrow(() -> new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND));
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemSearchRepositoryCustom.java b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemSearchRepositoryCustom.java
new file mode 100644
index 0000000..803ef2e
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemSearchRepositoryCustom.java
@@ -0,0 +1,97 @@
+package com.moplus.moplus_server.domain.problem.repository;
+
+import static com.moplus.moplus_server.domain.concept.domain.QConceptTag.conceptTag;
+import static com.moplus.moplus_server.domain.problem.domain.childProblem.QChildProblem.childProblem;
+import static com.moplus.moplus_server.domain.problem.domain.problem.QProblem.problem;
+
+import com.moplus.moplus_server.domain.problem.dto.response.ConceptTagSearchResponse;
+import com.moplus.moplus_server.domain.problem.dto.response.ProblemSearchGetResponse;
+import com.querydsl.core.group.GroupBy;
+import com.querydsl.core.types.Projections;
+import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.jpa.JPAExpressions;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class ProblemSearchRepositoryCustom {
+
+ private final JPAQueryFactory queryFactory;
+
+ public List search(String problemId, String title, List conceptTagIds) {
+ return queryFactory
+ .select(problem.problemCustomId.id, problem.title, problem.mainProblemImageUrl)
+ .from(problem)
+ .leftJoin(childProblem).on(childProblem.in(problem.childProblems))
+ .leftJoin(conceptTag).on(conceptTag.id.in(problem.conceptTagIds)
+ .or(conceptTag.id.in(childProblem.conceptTagIds)))
+ .where(
+ containsProblemId(problemId),
+ containsName(title),
+ hasConceptTags(conceptTagIds)
+ )
+ .distinct()
+ .transform(GroupBy.groupBy(problem.id).list(
+ Projections.constructor(ProblemSearchGetResponse.class,
+ problem.id,
+ problem.problemCustomId.id,
+ problem.title.title,
+ problem.memo,
+ problem.mainProblemImageUrl,
+ GroupBy.set(
+ Projections.constructor(ConceptTagSearchResponse.class,
+ conceptTag.id,
+ conceptTag.name
+ )
+ )
+ )
+ ));
+ }
+
+ private BooleanExpression hasConceptTags(List conceptTagIds) {
+ if (conceptTagIds == null || conceptTagIds.isEmpty()) {
+ return null;
+ }
+
+ return problem.id.in(
+ JPAExpressions
+ .selectFrom(problem)
+ .where(
+ problem.conceptTagIds.any().in(conceptTagIds)
+ .or(
+ problem.id.in(
+ JPAExpressions
+ .select(childProblem.id)
+ .from(childProblem)
+ .where(childProblem.conceptTagIds.any()
+ .in(conceptTagIds))
+ )
+ )
+ )
+ .select(problem.id)
+ );
+ }
+
+ //problemCustomId 일부 포함 검색
+ private BooleanExpression containsProblemId(String problemId) {
+ return (problemId == null || problemId.isEmpty()) ? null
+ : problem.problemCustomId.id.containsIgnoreCase(problemId);
+ }
+
+ //name 조건 (포함 검색)
+ private BooleanExpression containsName(String title) {
+ if (title == null || title.trim().isEmpty()) {
+ return null;
+ }
+ return problem.title.title.containsIgnoreCase(title.trim());
+ }
+
+ //conceptTagIds 조건 (하나라도 포함되면 조회)
+ private BooleanExpression inConceptTagIds(List conceptTagIds) {
+ return (conceptTagIds == null || conceptTagIds.isEmpty()) ? null
+ : problem.conceptTagIds.any().in(conceptTagIds);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/repository/converter/StringListConverter.java b/src/main/java/com/moplus/moplus_server/domain/problem/repository/converter/StringListConverter.java
new file mode 100644
index 0000000..9ca476b
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/repository/converter/StringListConverter.java
@@ -0,0 +1,41 @@
+package com.moplus.moplus_server.domain.problem.repository.converter;
+
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.InvalidValueException;
+import jakarta.persistence.AttributeConverter;
+import jakarta.persistence.Converter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Converter
+public class StringListConverter implements AttributeConverter, String> {
+
+ private static final String DELIMITER = ", ";
+
+ @Override
+ public String convertToDatabaseColumn(List attribute) {
+ try {
+ if (attribute == null || attribute.isEmpty()) {
+ return "";
+ }
+ return attribute.stream()
+ .filter(str -> str != null && !str.isEmpty())
+ .collect(Collectors.joining(DELIMITER));
+ } catch (Exception e) {
+ throw new InvalidValueException(ErrorCode.INVALID_INPUT_VALUE);
+ }
+ }
+
+ @Override
+ public List convertToEntityAttribute(String dbData) {
+ if (dbData == null || dbData.isEmpty()) {
+ return List.of();
+ }
+ return Arrays.stream(dbData.split(DELIMITER))
+ .filter(str -> str != null && !str.isEmpty())
+ .collect(Collectors.toList());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/ChildProblemService.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/ChildProblemService.java
new file mode 100644
index 0000000..bdc1f6a
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/ChildProblemService.java
@@ -0,0 +1,41 @@
+package com.moplus.moplus_server.domain.problem.service;
+
+import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem;
+import com.moplus.moplus_server.domain.problem.domain.problem.Problem;
+import com.moplus.moplus_server.domain.problem.repository.ChildProblemRepository;
+import com.moplus.moplus_server.domain.problem.repository.ProblemRepository;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.InvalidValueException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class ChildProblemService {
+
+ private final ProblemRepository problemRepository;
+ private final ChildProblemRepository childProblemRepository;
+
+ @Transactional
+ public Long createChildProblem(Long problemId) {
+ Problem problem = problemRepository.findByIdElseThrow(problemId);
+ if (problem.isConfirmed()) {
+ throw new InvalidValueException(ErrorCode.CHILD_PROBLEM_UPDATE_AFTER_CONFIRMED);
+ }
+
+ problem.addChildProblem(ChildProblem.createEmptyChildProblem());
+
+ return problemRepository.save(problem).getChildProblems().get(problem.getChildProblems().size() - 1).getId();
+ }
+
+ @Transactional
+ public void deleteChildProblem(Long problemId, Long childProblemId) {
+ Problem problem = problemRepository.findByIdElseThrow(problemId);
+ if (problem.isConfirmed()) {
+ throw new InvalidValueException(ErrorCode.CHILD_PROBLEM_UPDATE_AFTER_CONFIRMED);
+ }
+ problem.deleteChildProblem(childProblemId);
+ problemRepository.save(problem);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/ImageUploadService.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/ImageUploadService.java
new file mode 100644
index 0000000..634f602
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/ImageUploadService.java
@@ -0,0 +1,38 @@
+package com.moplus.moplus_server.domain.problem.service;
+
+import com.amazonaws.HttpMethod;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemImageType;
+import com.moplus.moplus_server.domain.problem.repository.ProblemRepository;
+import com.moplus.moplus_server.global.utils.s3.S3Util;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class ImageUploadService {
+
+ private static final String PROBLEM_IMAGE_PREFIX = "problems/";
+ private final S3Util s3Util;
+ private final ProblemRepository problemRepository;
+
+ public String generateProblemImagePresignedUrl(Long problemId, ProblemImageType imageType) {
+ problemRepository.existsByIdElseThrow(problemId);
+ String fileName = generateProblemImageFileName(problemId, imageType);
+ return s3Util.getS3PresignedUrl(fileName, HttpMethod.PUT);
+ }
+
+ private String generateProblemImageFileName(Long problemId, ProblemImageType imageType) {
+ String uuid = UUID.randomUUID().toString();
+ return String.format("%s%d/%s/%s.jpg",
+ PROBLEM_IMAGE_PREFIX,
+ problemId,
+ imageType.getType(),
+ uuid
+ );
+ }
+
+ public String getImageUrl(String fileName) {
+ return s3Util.getS3ObjectUrl(fileName);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemDeleteService.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemDeleteService.java
new file mode 100644
index 0000000..dc9c92d
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemDeleteService.java
@@ -0,0 +1,19 @@
+package com.moplus.moplus_server.domain.problem.service;
+
+import com.moplus.moplus_server.domain.problem.repository.ProblemRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class ProblemDeleteService {
+
+ private final ProblemRepository problemRepository;
+
+ @Transactional
+ public void deleteProblem(Long id) {
+ problemRepository.existsByIdElseThrow(id);
+ problemRepository.deleteById(id);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemGetService.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemGetService.java
new file mode 100644
index 0000000..a3b0fa1
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemGetService.java
@@ -0,0 +1,21 @@
+package com.moplus.moplus_server.domain.problem.service;
+
+import com.moplus.moplus_server.domain.problem.domain.problem.Problem;
+import com.moplus.moplus_server.domain.problem.dto.response.ProblemGetResponse;
+import com.moplus.moplus_server.domain.problem.repository.ProblemRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class ProblemGetService {
+
+ private final ProblemRepository problemRepository;
+
+ @Transactional(readOnly = true)
+ public ProblemGetResponse getProblem(Long problemId) {
+ Problem problem = problemRepository.findByIdElseThrow(problemId);
+ return ProblemGetResponse.of(problem);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveService.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveService.java
new file mode 100644
index 0000000..668233a
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveService.java
@@ -0,0 +1,43 @@
+package com.moplus.moplus_server.domain.problem.service;
+
+import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag;
+import com.moplus.moplus_server.domain.problem.domain.problem.Problem;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemAdminIdService;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemCustomId;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType;
+import com.moplus.moplus_server.domain.problem.dto.request.ProblemPostRequest;
+import com.moplus.moplus_server.domain.problem.dto.response.ProblemPostResponse;
+import com.moplus.moplus_server.domain.problem.repository.PracticeTestTagRepository;
+import com.moplus.moplus_server.domain.problem.repository.ProblemRepository;
+import com.moplus.moplus_server.domain.problem.service.mapper.ProblemMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class ProblemSaveService {
+
+ private final ProblemRepository problemRepository;
+ private final PracticeTestTagRepository practiceTestRepository;
+ private final ProblemAdminIdService problemAdminIdService;
+ private final ProblemMapper problemMapper;
+
+ @Transactional
+ public ProblemPostResponse createProblem(ProblemPostRequest request) {
+ Problem problem;
+ if (request.problemType() != ProblemType.CREATION_PROBLEM) {
+ PracticeTestTag practiceTestTag = practiceTestRepository.findByIdElseThrow(request.practiceTestId());
+ ProblemCustomId problemCustomId = problemAdminIdService.nextId(request.number(), practiceTestTag,
+ request.problemType());
+
+ problem = problemMapper.from(request, problemCustomId, practiceTestTag);
+ } else {
+ ProblemCustomId problemCustomId = problemAdminIdService.nextId(request.problemType());
+ problem = problemMapper.from(request.problemType(), problemCustomId);
+ }
+
+ return ProblemPostResponse.of(problemRepository.save(problem));
+ }
+
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateService.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateService.java
new file mode 100644
index 0000000..423fd3b
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateService.java
@@ -0,0 +1,54 @@
+package com.moplus.moplus_server.domain.problem.service;
+
+import com.moplus.moplus_server.domain.concept.repository.ConceptTagRepository;
+import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem;
+import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag;
+import com.moplus.moplus_server.domain.problem.domain.problem.Problem;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemAdminIdService;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemCustomId;
+import com.moplus.moplus_server.domain.problem.dto.request.ProblemUpdateRequest;
+import com.moplus.moplus_server.domain.problem.dto.response.ProblemGetResponse;
+import com.moplus.moplus_server.domain.problem.repository.PracticeTestTagRepository;
+import com.moplus.moplus_server.domain.problem.repository.ProblemRepository;
+import com.moplus.moplus_server.domain.problem.service.mapper.ChildProblemMapper;
+import com.moplus.moplus_server.domain.problem.service.mapper.ProblemMapper;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class ProblemUpdateService {
+
+ private final ProblemRepository problemRepository;
+ private final ProblemAdminIdService problemAdminIdService;
+ private final PracticeTestTagRepository practiceTestRepository;
+ private final ConceptTagRepository conceptTagRepository;
+ private final ChildProblemMapper childProblemMapper;
+ private final ProblemMapper problemMapper;
+
+ @Transactional
+ public ProblemGetResponse updateProblem(Long problemId, ProblemUpdateRequest request) {
+ PracticeTestTag practiceTestTag = practiceTestRepository.findByIdElseThrow(request.practiceTestId());
+ conceptTagRepository.existsByIdElseThrow(request.conceptTagIds());
+ Problem problem = problemRepository.findByIdElseThrow(problemId);
+
+ ProblemCustomId problemCustomId = problemAdminIdService.nextId(request.number(), practiceTestTag,
+ request.problemType());
+
+ Problem inputProblem = problemMapper.from(request, problemCustomId, practiceTestTag);
+ problem.update(inputProblem);
+
+ List childProblems = changeToChildProblems(request);
+ problem.updateChildProblem(childProblems);
+
+ return ProblemGetResponse.of(problemRepository.save(problem));
+ }
+
+ private List changeToChildProblems(ProblemUpdateRequest request) {
+ return request.updateChildProblems().stream()
+ .map(childProblemMapper::from)
+ .toList();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapper.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapper.java
new file mode 100644
index 0000000..b48abe5
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapper.java
@@ -0,0 +1,16 @@
+package com.moplus.moplus_server.domain.problem.service.mapper;
+
+import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem;
+import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemPostRequest;
+import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemUpdateRequest;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper(componentModel = "spring")
+public interface ChildProblemMapper {
+
+ ChildProblem from(ChildProblemPostRequest request);
+
+ @Mapping(target = "id", source = "childProblemId")
+ ChildProblem from(ChildProblemUpdateRequest request);
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapper.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapper.java
new file mode 100644
index 0000000..2f67b1d
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapper.java
@@ -0,0 +1,34 @@
+package com.moplus.moplus_server.domain.problem.service.mapper;
+
+import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag;
+import com.moplus.moplus_server.domain.problem.domain.problem.Problem;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemCustomId;
+import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType;
+import com.moplus.moplus_server.domain.problem.dto.request.ProblemPostRequest;
+import com.moplus.moplus_server.domain.problem.dto.request.ProblemUpdateRequest;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+
+@Mapper(componentModel = "spring")
+public interface ProblemMapper {
+
+ @Mappings({
+ @Mapping(target = "problemCustomId", source = "problemCustomId"),
+ @Mapping(target = "practiceTestTag", source = "practiceTestTag"),
+ })
+ Problem from(ProblemPostRequest request, ProblemCustomId problemCustomId, PracticeTestTag practiceTestTag);
+
+ @Mappings({
+ @Mapping(target = "problemCustomId", source = "problemCustomId"),
+ @Mapping(target = "practiceTestTag", source = "practiceTestTag"),
+ @Mapping(target = "recommendedMinute", source = "request.recommendedMinute"),
+ @Mapping(target = "recommendedSecond", source = "request.recommendedSecond")
+ })
+ Problem from(ProblemUpdateRequest request, ProblemCustomId problemCustomId, PracticeTestTag practiceTestTag);
+
+ @Mappings({
+ @Mapping(target = "problemCustomId", source = "problemCustomId")
+ })
+ Problem from(ProblemType problemType, ProblemCustomId problemCustomId);
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/controller/ProblemSetController.java b/src/main/java/com/moplus/moplus_server/domain/problemset/controller/ProblemSetController.java
new file mode 100644
index 0000000..c78c9f3
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/controller/ProblemSetController.java
@@ -0,0 +1,88 @@
+package com.moplus.moplus_server.domain.problemset.controller;
+
+import com.moplus.moplus_server.domain.problemset.domain.ProblemSetConfirmStatus;
+import com.moplus.moplus_server.domain.problemset.dto.request.ProblemReorderRequest;
+import com.moplus.moplus_server.domain.problemset.dto.request.ProblemSetUpdateRequest;
+import com.moplus.moplus_server.domain.problemset.dto.response.ProblemSetGetResponse;
+import com.moplus.moplus_server.domain.problemset.service.ProblemSetDeleteService;
+import com.moplus.moplus_server.domain.problemset.service.ProblemSetGetService;
+import com.moplus.moplus_server.domain.problemset.service.ProblemSetSaveService;
+import com.moplus.moplus_server.domain.problemset.service.ProblemSetUpdateService;
+import com.moplus.moplus_server.global.response.IdResponse;
+import io.swagger.v3.oas.annotations.Hidden;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "문항세트", description = "문항세트 관련 API")
+@RestController
+@RequestMapping("/api/v1/problemSet")
+@RequiredArgsConstructor
+public class ProblemSetController {
+
+ private final ProblemSetSaveService problemSetSaveService;
+ private final ProblemSetUpdateService problemSetUpdateService;
+ private final ProblemSetDeleteService problemSetDeleteService;
+ private final ProblemSetGetService problemSetGetService;
+
+ @PostMapping("")
+ @Operation(summary = "문항세트 생성", description = "문항세트를 생성합니다. 문항은 요청 순서대로 저장합니다.")
+ public ResponseEntity createProblemSet(
+ ) {
+ return ResponseEntity.ok(new IdResponse(problemSetSaveService.createProblemSet()));
+ }
+
+ @Hidden
+ @PutMapping("/{problemSetId}/sequence")
+ @Operation(summary = "세트 문항순서 변경", description = "문항세트 내의 문항 리스트의 순서를 변경합니다.")
+ public ResponseEntity reorderProblems(
+ @PathVariable Long problemSetId,
+ @RequestBody ProblemReorderRequest request) {
+ problemSetUpdateService.reorderProblems(problemSetId, request);
+ return ResponseEntity.noContent().build();
+ }
+
+ @PutMapping("/{problemSetId}")
+ @Operation(summary = "문항세트 수정", description = "문항세트의 이름 및 문항 리스트를 수정합니다.")
+ public ResponseEntity updateProblemSet(
+ @PathVariable Long problemSetId,
+ @RequestBody ProblemSetUpdateRequest request
+ ) {
+ problemSetUpdateService.updateProblemSet(problemSetId, request);
+ return ResponseEntity.ok(null);
+ }
+
+ @DeleteMapping("/{problemSetId}")
+ @Operation(summary = "문항세트 삭제", description = "문항세트를 삭제합니다. (soft delete)")
+ public ResponseEntity deleteProblemSet(
+ @PathVariable Long problemSetId
+ ) {
+ problemSetDeleteService.deleteProblemSet(problemSetId);
+ return ResponseEntity.ok(null);
+ }
+
+ @PutMapping("/{problemSetId}/confirm")
+ @Operation(summary = "문항세트 컨펌 토글", description = "문항세트의 컨펌 상태를 토글합니다.")
+ public ResponseEntity toggleConfirmProblemSet(
+ @PathVariable Long problemSetId
+ ) {
+ return ResponseEntity.ok(problemSetUpdateService.toggleConfirmProblemSet(problemSetId));
+ }
+
+ @GetMapping("/{problemSetId}")
+ @Operation(summary = "문항세트 개별 조회", description = "문항세트를 조회합니다.")
+ public ResponseEntity getProblemSet(
+ @PathVariable Long problemSetId
+ ) {
+ return ResponseEntity.ok(problemSetGetService.getProblemSet(problemSetId));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/controller/ProblemSetSearchController.java b/src/main/java/com/moplus/moplus_server/domain/problemset/controller/ProblemSetSearchController.java
new file mode 100644
index 0000000..0b65a47
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/controller/ProblemSetSearchController.java
@@ -0,0 +1,51 @@
+package com.moplus.moplus_server.domain.problemset.controller;
+
+
+import com.moplus.moplus_server.domain.problemset.dto.response.ProblemSetSearchGetResponse;
+import com.moplus.moplus_server.domain.problemset.repository.ProblemSetSearchRepositoryCustom;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/v1/problemSet")
+@RequiredArgsConstructor
+public class ProblemSetSearchController {
+
+ private final ProblemSetSearchRepositoryCustom problemSetSearchRepository;
+
+ @Tag(name = "문항세트", description = "문항세트 관련 API")
+ @GetMapping("/search")
+ @Operation(
+ summary = "문항세트 검색",
+ description = "문항세트 타이틀, 문항세트 내 포함된 개념태그, 문항세트 내 포함된 문항 타이틀로 검색합니다. 발행상태는 발행이면 CONFIRMED, 아니면 NOT_CONFIRMED 입니다."
+ )
+ public ResponseEntity> search(
+ @RequestParam(value = "problemSetTitle", required = false) String problemSetTitle,
+ @RequestParam(value = "problemTitle", required = false) String problemTitle
+ ) {
+ List problemSets = problemSetSearchRepository.search(problemSetTitle, problemTitle);
+ return ResponseEntity.ok(problemSets);
+ }
+
+ @Tag(name = "발행", description = "발행 관련 API")
+ @GetMapping("/confirm/search")
+ @Operation(
+ summary = "발행용 문항세트 검색",
+ description = "문항세트 타이틀, 문항세트 내 포함된 개념태그, 문항세트 내 포함된 문항 타이틀로 검색합니다. 발행상태가 CONFIRMED 문항세트만 조회됩니다.."
+ )
+ public ResponseEntity> confirmSearch(
+ @RequestParam(value = "problemSetTitle", required = false) String problemSetTitle,
+ @RequestParam(value = "problemTitle", required = false) String problemTitle
+ ) {
+ List problemSets = problemSetSearchRepository.confirmSearch(problemSetTitle,
+ problemTitle);
+ return ResponseEntity.ok(problemSets);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/domain/ProblemSet.java b/src/main/java/com/moplus/moplus_server/domain/problemset/domain/ProblemSet.java
new file mode 100644
index 0000000..077be3f
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/domain/ProblemSet.java
@@ -0,0 +1,94 @@
+package com.moplus.moplus_server.domain.problemset.domain;
+
+import com.moplus.moplus_server.domain.problem.domain.problem.Problem;
+import com.moplus.moplus_server.global.common.BaseEntity;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.InvalidValueException;
+import jakarta.persistence.CollectionTable;
+import jakarta.persistence.Column;
+import jakarta.persistence.ElementCollection;
+import jakarta.persistence.Embedded;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.OrderColumn;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class ProblemSet extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "problem_set_id")
+ Long id;
+
+ @Embedded
+ private Title title;
+
+ @Column(nullable = false)
+ private boolean isDeleted;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private ProblemSetConfirmStatus confirmStatus;
+
+ @ElementCollection
+ @CollectionTable(name = "problem_set_problems", joinColumns = @JoinColumn(name = "problem_set_id"))
+ @OrderColumn(name = "sequence")
+ @Column(name = "problem_id", nullable = false)
+ private List problemIds = new ArrayList<>();
+
+ @Builder
+ public ProblemSet(String title, List problemIds) {
+ this.title = new Title(title);
+ this.isDeleted = false;
+ this.confirmStatus = ProblemSetConfirmStatus.NOT_CONFIRMED;
+ this.problemIds = problemIds;
+ }
+
+ public static ProblemSet ofEmptyProblemSet() {
+ return ProblemSet.builder()
+ .title("")
+ .problemIds(new ArrayList<>())
+ .build();
+ }
+
+ public void updateProblemOrder(List newProblems) {
+ this.problemIds = new ArrayList<>(newProblems);
+ }
+
+ public void deleteProblemSet() {
+ this.isDeleted = true;
+ }
+
+ public void toggleConfirm(List problems) {
+ if (this.confirmStatus == ProblemSetConfirmStatus.NOT_CONFIRMED) {
+ List invalidProblemIds = problems.stream()
+ .filter(problem -> !problem.isValid())
+ .map(Problem::getProblemCustomId)
+ .toList();
+ if (!invalidProblemIds.isEmpty()) {
+ String message = ErrorCode.INVALID_CONFIRM_PROBLEM.getMessage() +
+ String.join("번 ", invalidProblemIds) + "번";
+ throw new InvalidValueException(message, ErrorCode.INVALID_CONFIRM_PROBLEM);
+ }
+ }
+ this.confirmStatus = this.confirmStatus.toggle();
+ }
+
+ public void updateProblemSet(String title, List newProblems) {
+ this.title = new Title(title);
+ this.problemIds = newProblems;
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/domain/ProblemSetConfirmStatus.java b/src/main/java/com/moplus/moplus_server/domain/problemset/domain/ProblemSetConfirmStatus.java
new file mode 100644
index 0000000..e108d26
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/domain/ProblemSetConfirmStatus.java
@@ -0,0 +1,9 @@
+package com.moplus.moplus_server.domain.problemset.domain;
+
+public enum ProblemSetConfirmStatus {
+ CONFIRMED,
+ NOT_CONFIRMED;
+ public ProblemSetConfirmStatus toggle() {
+ return this == CONFIRMED ? NOT_CONFIRMED : CONFIRMED;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/domain/Title.java b/src/main/java/com/moplus/moplus_server/domain/problemset/domain/Title.java
new file mode 100644
index 0000000..aa91e1c
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/domain/Title.java
@@ -0,0 +1,25 @@
+package com.moplus.moplus_server.domain.problemset.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Embeddable
+@NoArgsConstructor
+public class Title {
+
+ private static final String DEFAULT_TITLE = "제목 없음";
+
+ @Column(name = "title", nullable = false)
+ private String value;
+
+ public Title(String title) {
+ this.value = verifyTitle(title);
+ }
+
+ private static String verifyTitle(String title) {
+ return (title == null || title.trim().isEmpty()) ? DEFAULT_TITLE : title;
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemReorderRequest.java b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemReorderRequest.java
new file mode 100644
index 0000000..bd4e0fa
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemReorderRequest.java
@@ -0,0 +1,10 @@
+package com.moplus.moplus_server.domain.problemset.dto.request;
+
+import jakarta.validation.constraints.NotNull;
+import java.util.List;
+
+public record ProblemReorderRequest(
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ List newProblems
+) {
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemSetPostRequest.java b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemSetPostRequest.java
new file mode 100644
index 0000000..4b8a872
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemSetPostRequest.java
@@ -0,0 +1,18 @@
+package com.moplus.moplus_server.domain.problemset.dto.request;
+
+import com.moplus.moplus_server.domain.problemset.domain.ProblemSet;
+import jakarta.validation.constraints.NotNull;
+import java.util.List;
+
+public record ProblemSetPostRequest(
+ String problemSetTitle,
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ List problems
+) {
+ public ProblemSet toEntity(List problemIdList) {
+ return ProblemSet.builder()
+ .title(this.problemSetTitle)
+ .problemIds(problemIdList)
+ .build();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemSetUpdateRequest.java b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemSetUpdateRequest.java
new file mode 100644
index 0000000..97d6d48
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemSetUpdateRequest.java
@@ -0,0 +1,11 @@
+package com.moplus.moplus_server.domain.problemset.dto.request;
+
+import jakarta.validation.constraints.NotNull;
+import java.util.List;
+
+public record ProblemSetUpdateRequest(
+ String problemSetTitle,
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ List problemIds
+) {
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSetGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSetGetResponse.java
new file mode 100644
index 0000000..c4c47fa
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSetGetResponse.java
@@ -0,0 +1,31 @@
+package com.moplus.moplus_server.domain.problemset.dto.response;
+
+import com.moplus.moplus_server.domain.problemset.domain.ProblemSet;
+import com.moplus.moplus_server.domain.problemset.domain.ProblemSetConfirmStatus;
+import jakarta.validation.constraints.NotNull;
+import java.time.LocalDate;
+import java.util.List;
+import lombok.Builder;
+
+@Builder
+public record ProblemSetGetResponse(
+ Long id,
+ String title,
+ ProblemSetConfirmStatus confirmStatus,
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ List publishedDates,
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ List problemSummaries
+) {
+ public static ProblemSetGetResponse of(ProblemSet problemSet, List publishedDates,
+ List problemSummaries) {
+
+ return ProblemSetGetResponse.builder()
+ .id(problemSet.getId())
+ .title(problemSet.getTitle().getValue())
+ .confirmStatus(problemSet.getConfirmStatus())
+ .publishedDates(publishedDates)
+ .problemSummaries(problemSummaries)
+ .build();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSetSearchGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSetSearchGetResponse.java
new file mode 100644
index 0000000..dd2eef1
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSetSearchGetResponse.java
@@ -0,0 +1,28 @@
+package com.moplus.moplus_server.domain.problemset.dto.response;
+
+import com.moplus.moplus_server.domain.problemset.domain.ProblemSetConfirmStatus;
+import jakarta.validation.constraints.NotNull;
+import java.util.List;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+public class ProblemSetSearchGetResponse {
+ @NotNull(message = "ID 값은 필수입니다.")
+ private Long id;
+ private String problemSetTitle;
+ private ProblemSetConfirmStatus confirmStatus;
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ private List problemThumbnailResponses;
+
+ public ProblemSetSearchGetResponse(
+ Long id, String problemSetTitle, ProblemSetConfirmStatus confirmStatus,
+ List problemThumbnailResponses
+ ) {
+ this.id = id;
+ this.problemSetTitle = problemSetTitle;
+ this.confirmStatus = confirmStatus;
+ this.problemThumbnailResponses = problemThumbnailResponses;
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSummaryResponse.java b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSummaryResponse.java
new file mode 100644
index 0000000..427ec6c
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSummaryResponse.java
@@ -0,0 +1,31 @@
+package com.moplus.moplus_server.domain.problemset.dto.response;
+
+import com.moplus.moplus_server.domain.problem.domain.problem.Problem;
+import jakarta.validation.constraints.NotNull;
+import java.util.List;
+import lombok.Builder;
+
+@Builder
+public record ProblemSummaryResponse(
+ @NotNull(message = "문항 ID는 필수입니다")
+ Long problemId,
+ @NotNull(message = "문항 custom ID는 필수입니다")
+ String problemCustomId,
+ String problemTitle,
+ String memo,
+ String mainProblemImageUrl,
+ @NotNull(message = "컬렉션 값은 필수입니다.")
+ List tagNames
+) {
+ public static ProblemSummaryResponse of(Problem problem, List tagNames) {
+
+ return ProblemSummaryResponse.builder()
+ .problemId(problem.getId())
+ .problemCustomId(problem.getProblemCustomId())
+ .memo(problem.getMemo())
+ .problemTitle(problem.getTitle())
+ .mainProblemImageUrl(problem.getMainProblemImageUrl())
+ .tagNames(tagNames)
+ .build();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemThumbnailResponse.java b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemThumbnailResponse.java
new file mode 100644
index 0000000..2cbef8f
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemThumbnailResponse.java
@@ -0,0 +1,18 @@
+package com.moplus.moplus_server.domain.problemset.dto.response;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+public class ProblemThumbnailResponse {
+ private String problemTitle;
+ private String problemMemo;
+ private String mainProblemImageUrl;
+
+ public ProblemThumbnailResponse(String problemTitle, String problemMemo, String mainProblemImageUrl) {
+ this.problemTitle = problemTitle;
+ this.problemMemo = problemMemo;
+ this.mainProblemImageUrl = mainProblemImageUrl;
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/repository/ProblemSetRepository.java b/src/main/java/com/moplus/moplus_server/domain/problemset/repository/ProblemSetRepository.java
new file mode 100644
index 0000000..b145fad
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/repository/ProblemSetRepository.java
@@ -0,0 +1,22 @@
+package com.moplus.moplus_server.domain.problemset.repository;
+
+import com.moplus.moplus_server.domain.problemset.domain.ProblemSet;
+import com.moplus.moplus_server.domain.problemset.domain.ProblemSetConfirmStatus;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.NotFoundException;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ProblemSetRepository extends JpaRepository {
+
+ default ProblemSet findByIdElseThrow(Long problemSetId) {
+ return findById(problemSetId).orElseThrow(() -> new NotFoundException(ErrorCode.PROBLEM_SET_NOT_FOUND));
+ }
+
+ default void existsConfirmedActiveByIdElseThrow(Long problemSetId) {
+ if (!existsByIdAndIsDeletedFalseAndConfirmStatus(problemSetId, ProblemSetConfirmStatus.CONFIRMED)) {
+ throw new NotFoundException(ErrorCode.PROBLEM_SET_NOT_FOUND);
+ }
+ }
+
+ boolean existsByIdAndIsDeletedFalseAndConfirmStatus(Long problemSetId, ProblemSetConfirmStatus confirmStatus);
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/repository/ProblemSetSearchRepositoryCustom.java b/src/main/java/com/moplus/moplus_server/domain/problemset/repository/ProblemSetSearchRepositoryCustom.java
new file mode 100644
index 0000000..8ebb041
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/repository/ProblemSetSearchRepositoryCustom.java
@@ -0,0 +1,82 @@
+package com.moplus.moplus_server.domain.problemset.repository;
+
+import static com.moplus.moplus_server.domain.problem.domain.problem.QProblem.problem;
+import static com.moplus.moplus_server.domain.problemset.domain.ProblemSetConfirmStatus.CONFIRMED;
+import static com.moplus.moplus_server.domain.problemset.domain.QProblemSet.problemSet;
+
+import com.moplus.moplus_server.domain.problemset.dto.response.ProblemSetSearchGetResponse;
+import com.moplus.moplus_server.domain.problemset.dto.response.ProblemThumbnailResponse;
+import com.querydsl.core.group.GroupBy;
+import com.querydsl.core.types.Projections;
+import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class ProblemSetSearchRepositoryCustom {
+
+ private final JPAQueryFactory queryFactory;
+
+ public List search(String problemSetTitle, String problemTitle) {
+ return queryFactory
+ .from(problemSet)
+ .leftJoin(problem).on(problem.id.in(problemSet.problemIds)) // 문제 세트 내 포함된 문항과 조인
+ .where(
+ containsProblemSetTitle(problemSetTitle),
+ containsProblemTitle(problemTitle)
+ )
+ .distinct()
+ .transform(GroupBy.groupBy(problemSet.id).list(
+ Projections.constructor(ProblemSetSearchGetResponse.class,
+ problemSet.id,
+ problemSet.title.value,
+ problemSet.confirmStatus,
+ GroupBy.list(
+ Projections.constructor(ProblemThumbnailResponse.class,
+ problem.title.title,
+ problem.memo,
+ problem.mainProblemImageUrl
+ )
+ )
+ )
+ ));
+ }
+
+ public List confirmSearch(String problemSetTitle, String problemTitle) {
+ return queryFactory
+ .from(problemSet)
+ .leftJoin(problem).on(problem.id.in(problemSet.problemIds)) // 문제 세트 내 포함된 문항과 조인
+ .where(
+ problemSet.confirmStatus.eq(CONFIRMED),
+ containsProblemSetTitle(problemSetTitle),
+ containsProblemTitle(problemTitle)
+ )
+ .distinct()
+ .transform(GroupBy.groupBy(problemSet.id).list(
+ Projections.constructor(ProblemSetSearchGetResponse.class,
+ problemSet.id,
+ problemSet.title.value,
+ problemSet.confirmStatus,
+ GroupBy.list(
+ Projections.constructor(ProblemThumbnailResponse.class,
+ problem.title.title,
+ problem.memo,
+ problem.mainProblemImageUrl
+ )
+ )
+ )
+ ));
+ }
+
+ private BooleanExpression containsProblemSetTitle(String problemSetTitle) {
+ return (problemSetTitle == null || problemSetTitle.isEmpty()) ? null
+ : problemSet.title.value.containsIgnoreCase(problemSetTitle);
+ }
+
+ private BooleanExpression containsProblemTitle(String problemTitle) {
+ return (problemTitle == null || problemTitle.isEmpty()) ? null : problem.memo.containsIgnoreCase(problemTitle);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetDeleteService.java b/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetDeleteService.java
new file mode 100644
index 0000000..830d751
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetDeleteService.java
@@ -0,0 +1,21 @@
+package com.moplus.moplus_server.domain.problemset.service;
+
+import com.moplus.moplus_server.domain.problemset.domain.ProblemSet;
+import com.moplus.moplus_server.domain.problemset.repository.ProblemSetRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class ProblemSetDeleteService {
+
+ private final ProblemSetRepository problemSetRepository;
+
+ @Transactional
+ public void deleteProblemSet(Long problemSetId) {
+ ProblemSet problemSet = problemSetRepository.findByIdElseThrow(problemSetId);
+ problemSet.deleteProblemSet();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetGetService.java b/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetGetService.java
new file mode 100644
index 0000000..81553a0
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetGetService.java
@@ -0,0 +1,49 @@
+package com.moplus.moplus_server.domain.problemset.service;
+
+import com.moplus.moplus_server.domain.concept.domain.ConceptTag;
+import com.moplus.moplus_server.domain.concept.repository.ConceptTagRepository;
+import com.moplus.moplus_server.domain.problem.domain.problem.Problem;
+import com.moplus.moplus_server.domain.problem.repository.PracticeTestTagRepository;
+import com.moplus.moplus_server.domain.problem.repository.ProblemRepository;
+import com.moplus.moplus_server.domain.problemset.domain.ProblemSet;
+import com.moplus.moplus_server.domain.problemset.dto.response.ProblemSetGetResponse;
+import com.moplus.moplus_server.domain.problemset.dto.response.ProblemSummaryResponse;
+import com.moplus.moplus_server.domain.problemset.repository.ProblemSetRepository;
+import com.moplus.moplus_server.domain.publish.domain.Publish;
+import com.moplus.moplus_server.domain.publish.repository.PublishRepository;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class ProblemSetGetService {
+
+ private final ProblemSetRepository problemSetRepository;
+ private final ProblemRepository problemRepository;
+ private final ConceptTagRepository conceptTagRepository;
+ private final PublishRepository publishRepository;
+
+ @Transactional(readOnly = true)
+ public ProblemSetGetResponse getProblemSet(Long problemSetId) {
+
+ ProblemSet problemSet = problemSetRepository.findByIdElseThrow(problemSetId);
+ List publishedDates = publishRepository.findByProblemSetId(problemSetId).stream()
+ .map(Publish::getPublishedDate)
+ .toList();
+
+ List problemSummaries = new ArrayList<>();
+ for (Long problemId : problemSet.getProblemIds()) {
+ Problem problem = problemRepository.findByIdElseThrow(problemId);
+ List tagNames = conceptTagRepository.findAllByIdsElseThrow(problem.getConceptTagIds())
+ .stream()
+ .map(ConceptTag::getName)
+ .toList();
+ problemSummaries.add(ProblemSummaryResponse.of(problem, tagNames));
+ }
+ return ProblemSetGetResponse.of(problemSet, publishedDates, problemSummaries);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetSaveService.java b/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetSaveService.java
new file mode 100644
index 0000000..2281c09
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetSaveService.java
@@ -0,0 +1,26 @@
+package com.moplus.moplus_server.domain.problemset.service;
+
+import com.moplus.moplus_server.domain.problem.repository.ProblemRepository;
+import com.moplus.moplus_server.domain.problemset.domain.ProblemSet;
+import com.moplus.moplus_server.domain.problemset.repository.ProblemSetRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class ProblemSetSaveService {
+
+ private final ProblemSetRepository problemSetRepository;
+ private final ProblemRepository problemRepository;
+
+ @Transactional
+ public Long createProblemSet() {
+
+ // ProblemSet 생성
+ ProblemSet problemSet = ProblemSet.ofEmptyProblemSet();
+
+ return problemSetRepository.save(problemSet).getId();
+ }
+
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetUpdateService.java b/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetUpdateService.java
new file mode 100644
index 0000000..5e01a9c
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetUpdateService.java
@@ -0,0 +1,65 @@
+package com.moplus.moplus_server.domain.problemset.service;
+
+import com.moplus.moplus_server.domain.problem.domain.problem.Problem;
+import com.moplus.moplus_server.domain.problem.repository.ProblemRepository;
+import com.moplus.moplus_server.domain.problemset.domain.ProblemSet;
+import com.moplus.moplus_server.domain.problemset.domain.ProblemSetConfirmStatus;
+import com.moplus.moplus_server.domain.problemset.dto.request.ProblemReorderRequest;
+import com.moplus.moplus_server.domain.problemset.dto.request.ProblemSetUpdateRequest;
+import com.moplus.moplus_server.domain.problemset.repository.ProblemSetRepository;
+import com.moplus.moplus_server.domain.publish.domain.Publish;
+import com.moplus.moplus_server.domain.publish.repository.PublishRepository;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.InvalidValueException;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class ProblemSetUpdateService {
+
+ private final ProblemSetRepository problemSetRepository;
+ private final ProblemRepository problemRepository;
+ private final PublishRepository publishRepository;
+
+ @Transactional
+ public void reorderProblems(Long problemSetId, ProblemReorderRequest request) {
+ ProblemSet problemSet = problemSetRepository.findByIdElseThrow(problemSetId);
+
+ problemSet.updateProblemOrder(request.newProblems());
+ }
+
+ @Transactional
+ public void updateProblemSet(Long problemSetId, ProblemSetUpdateRequest request) {
+ ProblemSet problemSet = problemSetRepository.findByIdElseThrow(problemSetId);
+
+ // 빈 문항 유효성 검증
+ if (request.problemIds().isEmpty()) {
+ throw new InvalidValueException(ErrorCode.EMPTY_PROBLEMS_ERROR);
+ }
+
+ request.problemIds().forEach(problemRepository::findByIdElseThrow);
+
+ problemSet.updateProblemSet(request.problemSetTitle(), request.problemIds());
+ }
+
+ @Transactional
+ public ProblemSetConfirmStatus toggleConfirmProblemSet(Long problemSetId) {
+ ProblemSet problemSet = problemSetRepository.findByIdElseThrow(problemSetId);
+ List publishes = publishRepository.findByProblemSetId(problemSetId);
+ if (!publishes.isEmpty()) {
+ throw new InvalidValueException(ErrorCode.ALREADY_PUBLISHED_ERROR);
+ }
+
+ List problems = new ArrayList<>();
+ for (Long problemId : problemSet.getProblemIds()) {
+ Problem problem = problemRepository.findByIdElseThrow(problemId);
+ problems.add(problem);
+ }
+ problemSet.toggleConfirm(problems);
+ return problemSet.getConfirmStatus();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/publish/controller/PublishController.java b/src/main/java/com/moplus/moplus_server/domain/publish/controller/PublishController.java
new file mode 100644
index 0000000..7a53f8d
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/publish/controller/PublishController.java
@@ -0,0 +1,58 @@
+package com.moplus.moplus_server.domain.publish.controller;
+
+import com.moplus.moplus_server.domain.publish.dto.request.PublishPostRequest;
+import com.moplus.moplus_server.domain.publish.dto.response.PublishMonthGetResponse;
+import com.moplus.moplus_server.domain.publish.service.PublishDeleteService;
+import com.moplus.moplus_server.domain.publish.service.PublishGetService;
+import com.moplus.moplus_server.domain.publish.service.PublishSaveService;
+import com.moplus.moplus_server.global.response.IdResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+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.RestController;
+
+@Tag(name = "발행", description = "발행 관련 API")
+@RestController
+@RequestMapping("/api/v1/publish")
+@RequiredArgsConstructor
+public class PublishController {
+
+ private final PublishGetService publishGetService;
+ private final PublishSaveService publishSaveService;
+ private final PublishDeleteService publishDeleteService;
+
+ @GetMapping("/{year}/{month}")
+ @Operation(summary = "연월별 발행 조회", description = "연월별로 발행된 세트들을 조회합니다.")
+ public ResponseEntity> getPublishMonth(
+ @PathVariable int year,
+ @PathVariable int month
+ ) {
+ return ResponseEntity.ok(publishGetService.getPublishMonth(year, month));
+ }
+
+ @PostMapping("")
+ @Operation(summary = "발행 생성하기", description = "특정 날짜에 문항세트를 발행합니다.")
+ public ResponseEntity postPublish(
+ @Valid @RequestBody PublishPostRequest request
+ ) {
+ return ResponseEntity.ok(new IdResponse(publishSaveService.createPublish(request)));
+ }
+
+ @DeleteMapping("/{publishId}")
+ @Operation(summary = "발행 삭제", description = "발행을 삭제합니다.")
+ public ResponseEntity deleteProblemSet(
+ @PathVariable Long publishId
+ ) {
+ publishDeleteService.deletePublish(publishId);
+ return ResponseEntity.ok(null);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/publish/domain/Publish.java b/src/main/java/com/moplus/moplus_server/domain/publish/domain/Publish.java
new file mode 100644
index 0000000..3ea566c
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/publish/domain/Publish.java
@@ -0,0 +1,51 @@
+package com.moplus.moplus_server.domain.publish.domain;
+
+import com.moplus.moplus_server.global.common.BaseEntity;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.InvalidValueException;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import java.time.LocalDate;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Publish extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "publish_id")
+ private Long id;
+
+ @Column(nullable = false)
+ private LocalDate publishedDate;
+
+ @Column(name = "problem_set_id", nullable = false)
+ private Long problemSetId;
+
+ @Builder
+ public Publish(LocalDate publishedDate, Long problemSetId) {
+ this.publishedDate = publishedDate;
+ this.problemSetId = problemSetId;
+ }
+
+ public void validatePublishedDate() {
+ // 발행 시점 다음날부터 발행 가능
+ if (this.publishedDate.isBefore(LocalDate.now().plusDays(1))) {
+ throw new InvalidValueException(ErrorCode.INVALID_DATE_ERROR);
+ }
+ }
+
+ public void validateDeletable() {
+ if (this.publishedDate.isBefore(LocalDate.now())) {
+ throw new InvalidValueException(ErrorCode.CANNOT_DELETE_PAST_PUBLISH);
+ }
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/publish/dto/request/PublishPostRequest.java b/src/main/java/com/moplus/moplus_server/domain/publish/dto/request/PublishPostRequest.java
new file mode 100644
index 0000000..7d3960e
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/publish/dto/request/PublishPostRequest.java
@@ -0,0 +1,19 @@
+package com.moplus.moplus_server.domain.publish.dto.request;
+
+import com.moplus.moplus_server.domain.publish.domain.Publish;
+import jakarta.validation.constraints.NotNull;
+import java.time.LocalDate;
+
+public record PublishPostRequest(
+ @NotNull
+ LocalDate publishedDate,
+ @NotNull
+ Long problemSetId
+) {
+ public Publish toEntity() {
+ return Publish.builder()
+ .publishedDate(this.publishedDate)
+ .problemSetId(this.problemSetId)
+ .build();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/publish/dto/response/PublishMonthGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/publish/dto/response/PublishMonthGetResponse.java
new file mode 100644
index 0000000..8ca3189
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/publish/dto/response/PublishMonthGetResponse.java
@@ -0,0 +1,19 @@
+package com.moplus.moplus_server.domain.publish.dto.response;
+
+import com.moplus.moplus_server.domain.publish.domain.Publish;
+import lombok.Builder;
+
+@Builder
+public record PublishMonthGetResponse(
+ Long publishId,
+ int day,
+ PublishProblemSetResponse problemSetInfo
+) {
+ public static PublishMonthGetResponse of(Publish publish, PublishProblemSetResponse problemSetInfos) {
+ return PublishMonthGetResponse.builder()
+ .publishId(publish.getId())
+ .day(publish.getPublishedDate().getDayOfMonth())
+ .problemSetInfo(problemSetInfos)
+ .build();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/publish/dto/response/PublishProblemSetResponse.java b/src/main/java/com/moplus/moplus_server/domain/publish/dto/response/PublishProblemSetResponse.java
new file mode 100644
index 0000000..1afc053
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/publish/dto/response/PublishProblemSetResponse.java
@@ -0,0 +1,18 @@
+package com.moplus.moplus_server.domain.publish.dto.response;
+
+import com.moplus.moplus_server.domain.problemset.domain.ProblemSet;
+import lombok.Builder;
+
+@Builder
+public record PublishProblemSetResponse(
+ Long id,
+ String title
+) {
+ public static PublishProblemSetResponse of(ProblemSet problemSet) {
+
+ return PublishProblemSetResponse.builder()
+ .id(problemSet.getId())
+ .title(problemSet.getTitle().getValue())
+ .build();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/publish/repository/PublishRepository.java b/src/main/java/com/moplus/moplus_server/domain/publish/repository/PublishRepository.java
new file mode 100644
index 0000000..9168947
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/publish/repository/PublishRepository.java
@@ -0,0 +1,18 @@
+package com.moplus.moplus_server.domain.publish.repository;
+
+import com.moplus.moplus_server.domain.publish.domain.Publish;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.NotFoundException;
+import java.time.LocalDate;
+import java.util.List;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface PublishRepository extends JpaRepository {
+ List findByPublishedDateBetween(LocalDate startDate, LocalDate endDate);
+
+ default Publish findByIdElseThrow(Long publishId) {
+ return findById(publishId).orElseThrow(() -> new NotFoundException(ErrorCode.PUBLISH_NOT_FOUND));
+ }
+
+ List findByProblemSetId(Long problemSetId);
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/publish/service/PublishDeleteService.java b/src/main/java/com/moplus/moplus_server/domain/publish/service/PublishDeleteService.java
new file mode 100644
index 0000000..039ac00
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/publish/service/PublishDeleteService.java
@@ -0,0 +1,22 @@
+package com.moplus.moplus_server.domain.publish.service;
+
+import com.moplus.moplus_server.domain.publish.domain.Publish;
+import com.moplus.moplus_server.domain.publish.repository.PublishRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class PublishDeleteService {
+
+ private final PublishRepository publishRepository;
+
+ @Transactional
+ public void deletePublish(Long publishId) {
+ Publish publish = publishRepository.findByIdElseThrow(publishId);
+ publish.validateDeletable();
+
+ publishRepository.delete(publish);
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/publish/service/PublishGetService.java b/src/main/java/com/moplus/moplus_server/domain/publish/service/PublishGetService.java
new file mode 100644
index 0000000..bca2511
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/publish/service/PublishGetService.java
@@ -0,0 +1,65 @@
+package com.moplus.moplus_server.domain.publish.service;
+
+import com.moplus.moplus_server.domain.problemset.domain.ProblemSet;
+import com.moplus.moplus_server.domain.problemset.repository.ProblemSetRepository;
+import com.moplus.moplus_server.domain.publish.domain.Publish;
+import com.moplus.moplus_server.domain.publish.dto.response.PublishMonthGetResponse;
+import com.moplus.moplus_server.domain.publish.dto.response.PublishProblemSetResponse;
+import com.moplus.moplus_server.domain.publish.repository.PublishRepository;
+import com.moplus.moplus_server.global.error.exception.ErrorCode;
+import com.moplus.moplus_server.global.error.exception.InvalidValueException;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class PublishGetService {
+
+ private final PublishRepository publishRepository;
+ private final ProblemSetRepository problemSetRepository;
+
+ @Transactional(readOnly = true)
+ public List getPublishMonth(int year, int month) {
+ if (month < 1 || month > 12) {
+ throw new InvalidValueException(ErrorCode.INVALID_MONTH_ERROR);
+ }
+ LocalDate startDate = LocalDate.of(year, month, 1);
+ LocalDate endDate = startDate.withDayOfMonth(startDate.lengthOfMonth());
+
+ // 주어진 월에 해당하는 모든 Publish 조회
+ List publishes = publishRepository.findByPublishedDateBetween(startDate, endDate);
+
+ // 한 번의 쿼리로 모든 ProblemSet 조회
+ Map problemSetMap = getProblemSetMap(publishes);
+
+ return publishes.stream()
+ .map(publish -> convertToResponse(publish, problemSetMap))
+ .collect(Collectors.toList());
+ }
+
+ private Map getProblemSetMap(List publishes) {
+ List problemSetIds = publishes.stream()
+ .map(Publish::getProblemSetId)
+ .distinct()
+ .collect(Collectors.toList());
+
+ return problemSetRepository.findAllById(problemSetIds).stream()
+ .collect(Collectors.toMap(ProblemSet::getId, problemSet -> problemSet));
+ }
+
+ private PublishMonthGetResponse convertToResponse(Publish publish, Map problemSetMap) {
+ ProblemSet problemSet = problemSetMap.get(publish.getProblemSetId());
+ if (problemSet == null) {
+ throw new InvalidValueException(ErrorCode.PROBLEM_SET_NOT_FOUND);
+ }
+ return PublishMonthGetResponse.of(
+ publish,
+ PublishProblemSetResponse.of(problemSet)
+ );
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/publish/service/PublishSaveService.java b/src/main/java/com/moplus/moplus_server/domain/publish/service/PublishSaveService.java
new file mode 100644
index 0000000..d134080
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/publish/service/PublishSaveService.java
@@ -0,0 +1,26 @@
+package com.moplus.moplus_server.domain.publish.service;
+
+import com.moplus.moplus_server.domain.problemset.repository.ProblemSetRepository;
+import com.moplus.moplus_server.domain.publish.domain.Publish;
+import com.moplus.moplus_server.domain.publish.dto.request.PublishPostRequest;
+import com.moplus.moplus_server.domain.publish.repository.PublishRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class PublishSaveService {
+
+ private final ProblemSetRepository problemSetRepository;
+ private final PublishRepository publishRepository;
+
+ @Transactional
+ public Long createPublish(PublishPostRequest request) {
+ problemSetRepository.existsConfirmedActiveByIdElseThrow(request.problemSetId());
+ Publish publish = request.toEntity();
+ // 발행날짜 유효성 검사
+ publish.validatePublishedDate();
+ return publishRepository.save(publish).getId();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/controller/DetailResultApplicationController.java b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/controller/DetailResultApplicationController.java
similarity index 65%
rename from src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/controller/DetailResultApplicationController.java
rename to src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/controller/DetailResultApplicationController.java
index e497da5..a7edd83 100644
--- a/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/controller/DetailResultApplicationController.java
+++ b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/controller/DetailResultApplicationController.java
@@ -1,8 +1,9 @@
-package com.moplus.moplus_server.domain.DetailResultApplication.controller;
+package com.moplus.moplus_server.domain.v0.DetailResultApplication.controller;
-import com.moplus.moplus_server.domain.DetailResultApplication.dto.request.DetailResultApplicationPostRequest;
-import com.moplus.moplus_server.domain.DetailResultApplication.dto.response.ReviewNoteGetResponse;
-import com.moplus.moplus_server.domain.DetailResultApplication.service.DetailResultApplicationService;
+import com.moplus.moplus_server.domain.v0.DetailResultApplication.dto.request.DetailResultApplicationPostRequest;
+import com.moplus.moplus_server.domain.v0.DetailResultApplication.dto.response.ReviewNoteGetResponse;
+import com.moplus.moplus_server.domain.v0.DetailResultApplication.service.DetailResultApplicationService;
+import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
@@ -11,6 +12,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+@Hidden
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/detailResultApplication")
@@ -20,7 +22,8 @@ public class DetailResultApplicationController {
@PostMapping("")
@Operation(summary = "모의고사 결과 상세 분석서 신청하기")
- public ResponseEntity createApplication(@RequestBody DetailResultApplicationPostRequest request) {
+ public ResponseEntity createApplication(
+ @RequestBody DetailResultApplicationPostRequest request) {
detailResultApplicationService.saveApplication(request);
ReviewNoteGetResponse reviewNoteInfo = detailResultApplicationService.getReviewNoteInfo(request.testResultId());
return ResponseEntity.ok(reviewNoteInfo);
diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/dto/request/DetailResultApplicationPostRequest.java b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/dto/request/DetailResultApplicationPostRequest.java
new file mode 100644
index 0000000..2440e76
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/dto/request/DetailResultApplicationPostRequest.java
@@ -0,0 +1,18 @@
+package com.moplus.moplus_server.domain.v0.DetailResultApplication.dto.request;
+
+import com.moplus.moplus_server.domain.v0.DetailResultApplication.entity.DetailResultApplication;
+
+public record DetailResultApplicationPostRequest(
+ Long testResultId,
+ String name,
+ String phoneNumber
+) {
+
+ public DetailResultApplication toEntity() {
+ return DetailResultApplication.builder()
+ .testResultId(testResultId)
+ .name(name)
+ .phoneNumber(phoneNumber)
+ .build();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/dto/response/ProblemGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/dto/response/ProblemGetResponse.java
new file mode 100644
index 0000000..7fb14eb
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/dto/response/ProblemGetResponse.java
@@ -0,0 +1,30 @@
+package com.moplus.moplus_server.domain.v0.DetailResultApplication.dto.response;
+
+import com.moplus.moplus_server.domain.v0.practiceTest.domain.ProblemForTest;
+import com.moplus.moplus_server.domain.v0.practiceTest.domain.ProblemImageForTest;
+import com.moplus.moplus_server.domain.v0.practiceTest.domain.ProblemRating;
+import lombok.Builder;
+
+@Builder
+public record ProblemGetResponse(
+ String problemNumber,
+ String difficultLevel,
+ double correctRate,
+ String rating,
+ String imageUrl
+) {
+
+ public static ProblemGetResponse of(
+ ProblemForTest problemForTest
+ ) {
+ ProblemRating problemRating = problemForTest.getProblemRating();
+ ProblemImageForTest image = problemForTest.getImage();
+ return ProblemGetResponse.builder()
+ .problemNumber(problemForTest.getProblemNumber())
+ .difficultLevel(problemRating.getDifficultyLevel())
+ .correctRate(problemForTest.getCorrectRate())
+ .rating(problemRating.getRating())
+ .imageUrl(image.getImageUrl())
+ .build();
+ }
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/dto/response/ReviewNoteGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/dto/response/ReviewNoteGetResponse.java
similarity index 75%
rename from src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/dto/response/ReviewNoteGetResponse.java
rename to src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/dto/response/ReviewNoteGetResponse.java
index 95c8674..9fe3e97 100644
--- a/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/dto/response/ReviewNoteGetResponse.java
+++ b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/dto/response/ReviewNoteGetResponse.java
@@ -1,10 +1,8 @@
-package com.moplus.moplus_server.domain.DetailResultApplication.dto.response;
+package com.moplus.moplus_server.domain.v0.DetailResultApplication.dto.response;
-import com.moplus.moplus_server.domain.TestResult.dto.response.EstimatedRatingGetResponse;
-import com.moplus.moplus_server.domain.TestResult.dto.response.IncorrectProblemGetResponse;
-import com.moplus.moplus_server.domain.TestResult.dto.response.RatingTableGetResponse;
-import com.moplus.moplus_server.domain.TestResult.dto.response.TestResultGetResponse;
-import com.moplus.moplus_server.domain.TestResult.entity.TestResult;
+import com.moplus.moplus_server.domain.v0.TestResult.dto.response.EstimatedRatingGetResponse;
+import com.moplus.moplus_server.domain.v0.TestResult.dto.response.IncorrectProblemGetResponse;
+import com.moplus.moplus_server.domain.v0.TestResult.entity.TestResult;
import java.time.Duration;
import java.util.List;
import lombok.Builder;
diff --git a/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/entity/DetailResultApplication.java b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/entity/DetailResultApplication.java
similarity index 92%
rename from src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/entity/DetailResultApplication.java
rename to src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/entity/DetailResultApplication.java
index 0f9df6a..0b35102 100644
--- a/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/entity/DetailResultApplication.java
+++ b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/entity/DetailResultApplication.java
@@ -1,4 +1,4 @@
-package com.moplus.moplus_server.domain.DetailResultApplication.entity;
+package com.moplus.moplus_server.domain.v0.DetailResultApplication.entity;
import com.moplus.moplus_server.global.common.BaseEntity;
import jakarta.persistence.Column;
diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/respository/DetailResultApplicationRepository.java b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/respository/DetailResultApplicationRepository.java
new file mode 100644
index 0000000..b723fc9
--- /dev/null
+++ b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/respository/DetailResultApplicationRepository.java
@@ -0,0 +1,8 @@
+package com.moplus.moplus_server.domain.v0.DetailResultApplication.respository;
+
+import com.moplus.moplus_server.domain.v0.DetailResultApplication.entity.DetailResultApplication;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface DetailResultApplicationRepository extends JpaRepository {
+
+}
diff --git a/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/service/DetailResultApplicationService.java b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/service/DetailResultApplicationService.java
similarity index 61%
rename from src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/service/DetailResultApplicationService.java
rename to src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/service/DetailResultApplicationService.java
index 9765fe3..1382c1f 100644
--- a/src/main/java/com/moplus/moplus_server/domain/DetailResultApplication/service/DetailResultApplicationService.java
+++ b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/service/DetailResultApplicationService.java
@@ -1,20 +1,20 @@
-package com.moplus.moplus_server.domain.DetailResultApplication.service;
+package com.moplus.moplus_server.domain.v0.DetailResultApplication.service;
-import com.moplus.moplus_server.domain.DetailResultApplication.dto.request.DetailResultApplicationPostRequest;
-import com.moplus.moplus_server.domain.DetailResultApplication.dto.response.ProblemGetResponse;
-import com.moplus.moplus_server.domain.DetailResultApplication.dto.response.ReviewNoteGetResponse;
-import com.moplus.moplus_server.domain.DetailResultApplication.respository.DetailResultApplicationRepository;
-import com.moplus.moplus_server.domain.TestResult.dto.response.EstimatedRatingGetResponse;
-import com.moplus.moplus_server.domain.TestResult.entity.IncorrectProblem;
-import com.moplus.moplus_server.domain.TestResult.entity.TestResult;
-import com.moplus.moplus_server.domain.TestResult.repository.EstimatedRatingRepository;
-import com.moplus.moplus_server.domain.TestResult.repository.IncorrectProblemRepository;
-import com.moplus.moplus_server.domain.TestResult.repository.TestResultRepository;
-import com.moplus.moplus_server.domain.TestResult.service.IncorrectProblemService;
-import com.moplus.moplus_server.domain.practiceTest.domain.PracticeTest;
-import com.moplus.moplus_server.domain.practiceTest.domain.Problem;
-import com.moplus.moplus_server.domain.practiceTest.repository.PracticeTestRepository;
-import com.moplus.moplus_server.domain.practiceTest.repository.ProblemRepository;
+import com.moplus.moplus_server.domain.v0.DetailResultApplication.dto.request.DetailResultApplicationPostRequest;
+import com.moplus.moplus_server.domain.v0.DetailResultApplication.dto.response.ProblemGetResponse;
+import com.moplus.moplus_server.domain.v0.DetailResultApplication.dto.response.ReviewNoteGetResponse;
+import com.moplus.moplus_server.domain.v0.DetailResultApplication.respository.DetailResultApplicationRepository;
+import com.moplus.moplus_server.domain.v0.TestResult.dto.response.EstimatedRatingGetResponse;
+import com.moplus.moplus_server.domain.v0.TestResult.entity.IncorrectProblem;
+import com.moplus.moplus_server.domain.v0.TestResult.entity.TestResult;
+import com.moplus.moplus_server.domain.v0.TestResult.repository.EstimatedRatingRepository;
+import com.moplus.moplus_server.domain.v0.TestResult.repository.IncorrectProblemRepository;
+import com.moplus.moplus_server.domain.v0.TestResult.repository.TestResultRepository;
+import com.moplus.moplus_server.domain.v0.TestResult.service.IncorrectProblemService;
+import com.moplus.moplus_server.domain.v0.practiceTest.domain.PracticeTest;
+import com.moplus.moplus_server.domain.v0.practiceTest.domain.ProblemForTest;
+import com.moplus.moplus_server.domain.v0.practiceTest.repository.PracticeTestRepository;
+import com.moplus.moplus_server.domain.v0.practiceTest.repository.ProblemForTestRepository;
import com.moplus.moplus_server.global.error.exception.ErrorCode;
import com.moplus.moplus_server.global.error.exception.NotFoundException;
import java.time.Duration;
@@ -33,7 +33,7 @@ public class DetailResultApplicationService {
private final EstimatedRatingRepository estimatedRatingRepository;
private final IncorrectProblemService incorrectProblemService;
private final IncorrectProblemRepository incorrectProblemRepository;
- private final ProblemRepository problemRepository;
+ private final ProblemForTestRepository problemForTestRepository;
@Transactional
public void saveApplication(DetailResultApplicationPostRequest request) {
@@ -47,28 +47,30 @@ public ReviewNoteGetResponse getReviewNoteInfo(Long testResultId) {
PracticeTest practiceTest = getPracticeTestById(testResult.getPracticeTestId());
Duration averageSolvingTime = practiceTest.getAverageSolvingTime();
- List estimatedRatingGetResponses = estimatedRatingRepository.findAllByTestResultId(testResultId).stream()
+ List estimatedRatingGetResponses = estimatedRatingRepository.findAllByTestResultId(
+ testResultId).stream()
.map(EstimatedRatingGetResponse::from)
.toList();
int 대성마이맥_rating = estimatedRatingGetResponses.get(0).estimatedRating();
- List incorrectProblems = incorrectProblemRepository.findAllByTestResultId(testResultId).stream()
+ List