diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index ed48a4d0..23ec8a5a 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -27,6 +27,9 @@ services: - 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} + - KAKAO_LOGIN_CLIENT_ID=${KAKAO_LOGIN_CLIENT_ID} + - KAKAO_LOGIN_CLIENT_SECRET=${KAKAO_LOGIN_CLIENT_SECRET} + - KAKAO_LOGIN_REDIRECT_URI=${KAKAO_LOGIN_DEV_REDIRECT_URI} depends_on: - mysql diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index e79df62f..6c03ee07 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -27,6 +27,9 @@ services: - 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} + - KAKAO_LOGIN_CLIENT_ID=${KAKAO_LOGIN_CLIENT_ID} + - KAKAO_LOGIN_CLIENT_SECRET=${KAKAO_LOGIN_CLIENT_SECRET} + - KAKAO_LOGIN_REDIRECT_URI=${KAKAO_LOGIN_PROD_REDIRECT_URI} depends_on: - mysql networks: diff --git a/src/main/generated/com/moplus/moplus_server/domain/publish/domain/QPublish.java b/src/main/generated/com/moplus/moplus_server/admin/publish/domain/QPublish.java similarity index 92% rename from src/main/generated/com/moplus/moplus_server/domain/publish/domain/QPublish.java rename to src/main/generated/com/moplus/moplus_server/admin/publish/domain/QPublish.java index 41676a40..5e607379 100644 --- a/src/main/generated/com/moplus/moplus_server/domain/publish/domain/QPublish.java +++ b/src/main/generated/com/moplus/moplus_server/admin/publish/domain/QPublish.java @@ -1,4 +1,4 @@ -package com.moplus.moplus_server.domain.publish.domain; +package com.moplus.moplus_server.admin.publish.domain; import static com.querydsl.core.types.PathMetadataFactory.*; @@ -15,7 +15,7 @@ @Generated("com.querydsl.codegen.DefaultEntitySerializer") public class QPublish extends EntityPathBase { - private static final long serialVersionUID = 1565569153L; + private static final long serialVersionUID = 1641302032L; public static final QPublish publish = new QPublish("publish"); diff --git a/src/main/generated/com/moplus/moplus_server/client/submit/domain/QChildProblemSubmit.java b/src/main/generated/com/moplus/moplus_server/client/submit/domain/QChildProblemSubmit.java new file mode 100644 index 00000000..1d70b647 --- /dev/null +++ b/src/main/generated/com/moplus/moplus_server/client/submit/domain/QChildProblemSubmit.java @@ -0,0 +1,53 @@ +package com.moplus.moplus_server.client.submit.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; + + +/** + * QChildProblemSubmit is a Querydsl query type for ChildProblemSubmit + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QChildProblemSubmit extends EntityPathBase { + + private static final long serialVersionUID = -1656142683L; + + public static final QChildProblemSubmit childProblemSubmit = new QChildProblemSubmit("childProblemSubmit"); + + public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this); + + public final NumberPath childProblemId = createNumber("childProblemId", Long.class); + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + public final NumberPath id = createNumber("id", Long.class); + + public final NumberPath memberId = createNumber("memberId", Long.class); + + public final NumberPath publishId = createNumber("publishId", Long.class); + + public final EnumPath status = createEnum("status", ChildProblemSubmitStatus.class); + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public QChildProblemSubmit(String variable) { + super(ChildProblemSubmit.class, forVariable(variable)); + } + + public QChildProblemSubmit(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QChildProblemSubmit(PathMetadata metadata) { + super(ChildProblemSubmit.class, metadata); + } + +} + diff --git a/src/main/generated/com/moplus/moplus_server/client/submit/domain/QProblemSubmit.java b/src/main/generated/com/moplus/moplus_server/client/submit/domain/QProblemSubmit.java new file mode 100644 index 00000000..e573b013 --- /dev/null +++ b/src/main/generated/com/moplus/moplus_server/client/submit/domain/QProblemSubmit.java @@ -0,0 +1,53 @@ +package com.moplus.moplus_server.client.submit.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; + + +/** + * QProblemSubmit is a Querydsl query type for ProblemSubmit + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QProblemSubmit extends EntityPathBase { + + private static final long serialVersionUID = 1682818189L; + + public static final QProblemSubmit problemSubmit = new QProblemSubmit("problemSubmit"); + + 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 memberId = createNumber("memberId", Long.class); + + public final NumberPath problemId = createNumber("problemId", Long.class); + + public final NumberPath publishId = createNumber("publishId", Long.class); + + public final EnumPath status = createEnum("status", ProblemSubmitStatus.class); + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public QProblemSubmit(String variable) { + super(ProblemSubmit.class, forVariable(variable)); + } + + public QProblemSubmit(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QProblemSubmit(PathMetadata metadata) { + super(ProblemSubmit.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 index f872cbc1..7b1bd0c8 100644 --- 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 @@ -1,7 +1,7 @@ package com.moplus.moplus_server.domain.problem.service.mapper; +import com.moplus.moplus_server.admin.problem.dto.request.ChildProblemUpdateRequest; import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem; -import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemUpdateRequest; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; @@ -11,7 +11,7 @@ @Generated( value = "org.mapstruct.ap.MappingProcessor", - date = "2025-03-03T17:28:35+0900", + date = "2025-03-24T01:02:22+0900", comments = "version: 1.6.3, compiler: javac, environment: Java 17.0.10 (JetBrains s.r.o.)" ) @Component 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 index e2bc255c..6992d6a4 100644 --- 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 @@ -1,11 +1,11 @@ package com.moplus.moplus_server.domain.problem.service.mapper; +import com.moplus.moplus_server.admin.problem.dto.request.ProblemPostRequest; +import com.moplus.moplus_server.admin.problem.dto.request.ProblemUpdateRequest; 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; @@ -15,7 +15,7 @@ @Generated( value = "org.mapstruct.ap.MappingProcessor", - date = "2025-03-03T17:28:35+0900", + date = "2025-03-24T01:02:22+0900", comments = "version: 1.6.3, compiler: javac, environment: Java 17.0.10 (JetBrains s.r.o.)" ) @Component 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 index 9f3b9956..b73be8c0 100644 --- 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 @@ -37,7 +37,7 @@ public class QIncorrectProblem extends EntityPathBase { public final NumberPath practiceTestId = createNumber("practiceTestId", Long.class); - public final NumberPath problemId = createNumber("problemCustomId", Long.class); + public final NumberPath problemId = createNumber("problemId", Long.class); public final StringPath problemNumber = createString("problemNumber"); 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 index 67b69aa3..b7e7668a 100644 --- 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 @@ -25,7 +25,7 @@ public class QProblemImageForTest extends EntityPathBase { public final StringPath imageUrl = createString("imageUrl"); - public final NumberPath problemId = createNumber("problemCustomId", Long.class); + public final NumberPath problemId = createNumber("problemId", Long.class); public QProblemImageForTest(String variable) { super(ProblemImageForTest.class, forVariable(variable)); diff --git a/src/main/generated/com/moplus/moplus_server/domain/member/domain/QMember.java b/src/main/generated/com/moplus/moplus_server/member/domain/QMember.java similarity index 61% rename from src/main/generated/com/moplus/moplus_server/domain/member/domain/QMember.java rename to src/main/generated/com/moplus/moplus_server/member/domain/QMember.java index ad5aeae6..69c07415 100644 --- a/src/main/generated/com/moplus/moplus_server/domain/member/domain/QMember.java +++ b/src/main/generated/com/moplus/moplus_server/member/domain/QMember.java @@ -1,4 +1,4 @@ -package com.moplus.moplus_server.domain.member.domain; +package com.moplus.moplus_server.member.domain; import static com.querydsl.core.types.PathMetadataFactory.*; @@ -7,6 +7,7 @@ import com.querydsl.core.types.PathMetadata; import javax.annotation.processing.Generated; import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; /** @@ -15,7 +16,9 @@ @Generated("com.querydsl.codegen.DefaultEntitySerializer") public class QMember extends EntityPathBase { - private static final long serialVersionUID = -705761779L; + private static final long serialVersionUID = -1541748259L; + + private static final PathInits INITS = PathInits.DIRECT2; public static final QMember member = new QMember("member1"); @@ -30,6 +33,8 @@ public class QMember extends EntityPathBase { public final StringPath name = createString("name"); + public final QOauthInfo oauthInfo; + public final StringPath password = createString("password"); public final EnumPath role = createEnum("role", MemberRole.class); @@ -38,15 +43,24 @@ public class QMember extends EntityPathBase { public final DateTimePath updatedDate = _super.updatedDate; public QMember(String variable) { - super(Member.class, forVariable(variable)); + this(Member.class, forVariable(variable), INITS); } public QMember(Path path) { - super(path.getType(), path.getMetadata()); + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); } public QMember(PathMetadata metadata) { - super(Member.class, metadata); + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QMember(PathMetadata metadata, PathInits inits) { + this(Member.class, metadata, inits); + } + + public QMember(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.oauthInfo = inits.isInitialized("oauthInfo") ? new QOauthInfo(forProperty("oauthInfo")) : null; } } diff --git a/src/main/generated/com/moplus/moplus_server/member/domain/QOauthInfo.java b/src/main/generated/com/moplus/moplus_server/member/domain/QOauthInfo.java new file mode 100644 index 00000000..f9a677a4 --- /dev/null +++ b/src/main/generated/com/moplus/moplus_server/member/domain/QOauthInfo.java @@ -0,0 +1,43 @@ +package com.moplus.moplus_server.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; + + +/** + * QOauthInfo is a Querydsl query type for OauthInfo + */ +@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer") +public class QOauthInfo extends BeanPath { + + private static final long serialVersionUID = 1483708866L; + + public static final QOauthInfo oauthInfo = new QOauthInfo("oauthInfo"); + + public final StringPath name = createString("name"); + + public final StringPath oauthEmail = createString("oauthEmail"); + + public final StringPath oauthId = createString("oauthId"); + + public final StringPath oauthProvider = createString("oauthProvider"); + + public QOauthInfo(String variable) { + super(OauthInfo.class, forVariable(variable)); + } + + public QOauthInfo(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QOauthInfo(PathMetadata metadata) { + super(OauthInfo.class, metadata); + } + +} + diff --git a/src/main/generated/com/moplus/moplus_server/statistic/Problem/domain/QChildProblemStatistic.java b/src/main/generated/com/moplus/moplus_server/statistic/Problem/domain/QChildProblemStatistic.java new file mode 100644 index 00000000..a77720c4 --- /dev/null +++ b/src/main/generated/com/moplus/moplus_server/statistic/Problem/domain/QChildProblemStatistic.java @@ -0,0 +1,53 @@ +package com.moplus.moplus_server.statistic.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; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QChildProblemStatistic is a Querydsl query type for ChildProblemStatistic + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QChildProblemStatistic extends EntityPathBase { + + private static final long serialVersionUID = 1136828573L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QChildProblemStatistic childProblemStatistic = new QChildProblemStatistic("childProblemStatistic"); + + public final NumberPath childProblemId = createNumber("childProblemId", Long.class); + + public final QCountStatistic countStatistic; + + public final NumberPath id = createNumber("id", Long.class); + + public QChildProblemStatistic(String variable) { + this(ChildProblemStatistic.class, forVariable(variable), INITS); + } + + public QChildProblemStatistic(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QChildProblemStatistic(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QChildProblemStatistic(PathMetadata metadata, PathInits inits) { + this(ChildProblemStatistic.class, metadata, inits); + } + + public QChildProblemStatistic(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.countStatistic = inits.isInitialized("countStatistic") ? new QCountStatistic(forProperty("countStatistic")) : null; + } + +} + diff --git a/src/main/generated/com/moplus/moplus_server/statistic/Problem/domain/QCountStatistic.java b/src/main/generated/com/moplus/moplus_server/statistic/Problem/domain/QCountStatistic.java new file mode 100644 index 00000000..5b0ba49d --- /dev/null +++ b/src/main/generated/com/moplus/moplus_server/statistic/Problem/domain/QCountStatistic.java @@ -0,0 +1,39 @@ +package com.moplus.moplus_server.statistic.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; + + +/** + * QCountStatistic is a Querydsl query type for CountStatistic + */ +@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer") +public class QCountStatistic extends BeanPath { + + private static final long serialVersionUID = 1047466257L; + + public static final QCountStatistic countStatistic = new QCountStatistic("countStatistic"); + + public final NumberPath submitCount = createNumber("submitCount", Long.class); + + public final NumberPath viewCount = createNumber("viewCount", Long.class); + + public QCountStatistic(String variable) { + super(CountStatistic.class, forVariable(variable)); + } + + public QCountStatistic(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QCountStatistic(PathMetadata metadata) { + super(CountStatistic.class, metadata); + } + +} + diff --git a/src/main/generated/com/moplus/moplus_server/statistic/Problem/domain/QProblemSetStatistic.java b/src/main/generated/com/moplus/moplus_server/statistic/Problem/domain/QProblemSetStatistic.java new file mode 100644 index 00000000..6d992ad2 --- /dev/null +++ b/src/main/generated/com/moplus/moplus_server/statistic/Problem/domain/QProblemSetStatistic.java @@ -0,0 +1,53 @@ +package com.moplus.moplus_server.statistic.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; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QProblemSetStatistic is a Querydsl query type for ProblemSetStatistic + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QProblemSetStatistic extends EntityPathBase { + + private static final long serialVersionUID = -1319940803L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QProblemSetStatistic problemSetStatistic = new QProblemSetStatistic("problemSetStatistic"); + + public final QCountStatistic countStatistic; + + public final NumberPath id = createNumber("id", Long.class); + + public final NumberPath problemSetId = createNumber("problemSetId", Long.class); + + public QProblemSetStatistic(String variable) { + this(ProblemSetStatistic.class, forVariable(variable), INITS); + } + + public QProblemSetStatistic(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QProblemSetStatistic(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QProblemSetStatistic(PathMetadata metadata, PathInits inits) { + this(ProblemSetStatistic.class, metadata, inits); + } + + public QProblemSetStatistic(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.countStatistic = inits.isInitialized("countStatistic") ? new QCountStatistic(forProperty("countStatistic")) : null; + } + +} + diff --git a/src/main/generated/com/moplus/moplus_server/statistic/Problem/domain/QProblemStatistic.java b/src/main/generated/com/moplus/moplus_server/statistic/Problem/domain/QProblemStatistic.java new file mode 100644 index 00000000..8eae98c4 --- /dev/null +++ b/src/main/generated/com/moplus/moplus_server/statistic/Problem/domain/QProblemStatistic.java @@ -0,0 +1,53 @@ +package com.moplus.moplus_server.statistic.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; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QProblemStatistic is a Querydsl query type for ProblemStatistic + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QProblemStatistic extends EntityPathBase { + + private static final long serialVersionUID = 843488641L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QProblemStatistic problemStatistic = new QProblemStatistic("problemStatistic"); + + public final QCountStatistic countStatistic; + + public final NumberPath id = createNumber("id", Long.class); + + public final NumberPath problemId = createNumber("problemId", Long.class); + + public QProblemStatistic(String variable) { + this(ProblemStatistic.class, forVariable(variable), INITS); + } + + public QProblemStatistic(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QProblemStatistic(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QProblemStatistic(PathMetadata metadata, PathInits inits) { + this(ProblemStatistic.class, metadata, inits); + } + + public QProblemStatistic(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.countStatistic = inits.isInitialized("countStatistic") ? new QCountStatistic(forProperty("countStatistic")) : null; + } + +} + diff --git a/src/main/generated/com/moplus/moplus_server/statistic/member/QCorrectConceptTagStatistics.java b/src/main/generated/com/moplus/moplus_server/statistic/member/QCorrectConceptTagStatistics.java new file mode 100644 index 00000000..1c5c5f93 --- /dev/null +++ b/src/main/generated/com/moplus/moplus_server/statistic/member/QCorrectConceptTagStatistics.java @@ -0,0 +1,49 @@ +package com.moplus.moplus_server.statistic.member; + +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; + + +/** + * QCorrectConceptTagStatistics is a Querydsl query type for CorrectConceptTagStatistics + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QCorrectConceptTagStatistics extends EntityPathBase { + + private static final long serialVersionUID = -313521578L; + + public static final QCorrectConceptTagStatistics correctConceptTagStatistics = new QCorrectConceptTagStatistics("correctConceptTagStatistics"); + + public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this); + + public final NumberPath conceptTagId = createNumber("conceptTagId", Long.class); + + public final NumberPath correctCount = createNumber("correctCount", Integer.class); + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + public final NumberPath id = createNumber("id", Long.class); + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public QCorrectConceptTagStatistics(String variable) { + super(CorrectConceptTagStatistics.class, forVariable(variable)); + } + + public QCorrectConceptTagStatistics(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QCorrectConceptTagStatistics(PathMetadata metadata) { + super(CorrectConceptTagStatistics.class, metadata); + } + +} + diff --git a/src/main/generated/com/moplus/moplus_server/statistic/member/QIncorrectConceptTagStatistics.java b/src/main/generated/com/moplus/moplus_server/statistic/member/QIncorrectConceptTagStatistics.java new file mode 100644 index 00000000..7222e577 --- /dev/null +++ b/src/main/generated/com/moplus/moplus_server/statistic/member/QIncorrectConceptTagStatistics.java @@ -0,0 +1,49 @@ +package com.moplus.moplus_server.statistic.member; + +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; + + +/** + * QIncorrectConceptTagStatistics is a Querydsl query type for IncorrectConceptTagStatistics + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QIncorrectConceptTagStatistics extends EntityPathBase { + + private static final long serialVersionUID = 1288752209L; + + public static final QIncorrectConceptTagStatistics incorrectConceptTagStatistics = new QIncorrectConceptTagStatistics("incorrectConceptTagStatistics"); + + public final com.moplus.moplus_server.global.common.QBaseEntity _super = new com.moplus.moplus_server.global.common.QBaseEntity(this); + + public final NumberPath conceptTagId = createNumber("conceptTagId", Long.class); + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + public final NumberPath id = createNumber("id", Long.class); + + public final NumberPath incorrectCount = createNumber("incorrectCount", Integer.class); + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public QIncorrectConceptTagStatistics(String variable) { + super(IncorrectConceptTagStatistics.class, forVariable(variable)); + } + + public QIncorrectConceptTagStatistics(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QIncorrectConceptTagStatistics(PathMetadata metadata) { + super(IncorrectConceptTagStatistics.class, metadata); + } + +} + diff --git a/src/main/java/com/moplus/moplus_server/admin/publish/service/PublishGetService.java b/src/main/java/com/moplus/moplus_server/admin/publish/service/PublishGetService.java index d15f2c20..d440d293 100644 --- a/src/main/java/com/moplus/moplus_server/admin/publish/service/PublishGetService.java +++ b/src/main/java/com/moplus/moplus_server/admin/publish/service/PublishGetService.java @@ -1,10 +1,10 @@ package com.moplus.moplus_server.admin.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.admin.publish.domain.Publish; import com.moplus.moplus_server.admin.publish.dto.response.PublishMonthGetResponse; import com.moplus.moplus_server.admin.publish.dto.response.PublishProblemSetResponse; +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.repository.PublishRepository; import com.moplus.moplus_server.global.error.exception.ErrorCode; import com.moplus.moplus_server.global.error.exception.InvalidValueException; @@ -42,6 +42,7 @@ public List getPublishMonth(int year, int month) { .collect(Collectors.toList()); } + private Map getProblemSetMap(List publishes) { List problemSetIds = publishes.stream() .map(Publish::getProblemSetId) @@ -62,4 +63,9 @@ private PublishMonthGetResponse convertToResponse(Publish publish, Map getPublishesBetweenDates(LocalDate startDate, LocalDate endDate) { + return publishRepository.findByPublishedDateBetween(startDate, endDate); + } } diff --git a/src/main/java/com/moplus/moplus_server/client/homefeed/controller/HomeFeedController.java b/src/main/java/com/moplus/moplus_server/client/homefeed/controller/HomeFeedController.java new file mode 100644 index 00000000..2e1bc012 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/homefeed/controller/HomeFeedController.java @@ -0,0 +1,27 @@ +package com.moplus.moplus_server.client.homefeed.controller; + +import com.moplus.moplus_server.client.homefeed.dto.response.HomeFeedResponse; +import com.moplus.moplus_server.client.homefeed.service.HomeFeedFacadeService; +import com.moplus.moplus_server.global.annotation.AuthUser; +import com.moplus.moplus_server.member.domain.Member; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +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 +@RequiredArgsConstructor +@RequestMapping("/api/v1/client/home-feed") +public class HomeFeedController { + + private final HomeFeedFacadeService homeFeedFacadeService; + + @Operation(summary = "홈 피드 조회", description = "회원의 홈 피드 정보를 조회합니다.") + @GetMapping("") + public HomeFeedResponse getHomeFeed(@AuthUser Member member) { + return homeFeedFacadeService.getHomeFeed(member); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/homefeed/dto/response/HomeFeedResponse.java b/src/main/java/com/moplus/moplus_server/client/homefeed/dto/response/HomeFeedResponse.java new file mode 100644 index 00000000..7699cb6a --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/homefeed/dto/response/HomeFeedResponse.java @@ -0,0 +1,70 @@ +package com.moplus.moplus_server.client.homefeed.dto.response; + +import com.moplus.moplus_server.admin.problemset.dto.response.ProblemSetGetResponse; +import com.moplus.moplus_server.admin.problemset.dto.response.ProblemSummaryResponse; +import com.moplus.moplus_server.client.submit.domain.ProgressStatus; +import java.time.LocalDate; +import java.util.List; + +public record HomeFeedResponse( + List dailyProgresses, + List problemSets +) { + public static HomeFeedResponse of( + List dailyProgresses, + List problemSets + ) { + return new HomeFeedResponse(dailyProgresses, problemSets); + } + + public record DailyProgressResponse( + LocalDate date, + ProgressStatus progressStatus + ) { + public static DailyProgressResponse of(LocalDate date, ProgressStatus progressStatus) { + return new DailyProgressResponse(date, progressStatus); + } + } + + public record ProblemSetHomeFeedResponse( + LocalDate date, + Long publishId, + String title, + Long submitCount, + ProblemHomeFeedResponse problemHomeFeedResponse + ) { + public static ProblemSetHomeFeedResponse of(LocalDate date, Long publishId, + ProblemSetGetResponse problemSetGetResponse, + Long submitCount) { + return new ProblemSetHomeFeedResponse( + date, + publishId, + problemSetGetResponse.title(), + submitCount, + ProblemHomeFeedResponse.of(problemSetGetResponse.problemSummaries().get(0)) + ); + } + + public static ProblemSetHomeFeedResponse of(LocalDate date) { + return new ProblemSetHomeFeedResponse( + date, + null, + null, + null, + null + ); + } + } + + public record ProblemHomeFeedResponse( + Long problemId, + String mainProblemImageUrl + ) { + public static ProblemHomeFeedResponse of(ProblemSummaryResponse problemSummaryResponse) { + return new ProblemHomeFeedResponse( + problemSummaryResponse.problemId(), + problemSummaryResponse.mainProblemImageUrl() + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/moplus/moplus_server/client/homefeed/service/HomeFeedFacadeService.java b/src/main/java/com/moplus/moplus_server/client/homefeed/service/HomeFeedFacadeService.java new file mode 100644 index 00000000..4a53ef4a --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/homefeed/service/HomeFeedFacadeService.java @@ -0,0 +1,91 @@ +package com.moplus.moplus_server.client.homefeed.service; + +import com.moplus.moplus_server.admin.problemset.dto.response.ProblemSetGetResponse; +import com.moplus.moplus_server.admin.publish.domain.Publish; +import com.moplus.moplus_server.admin.publish.service.PublishGetService; +import com.moplus.moplus_server.client.homefeed.dto.response.HomeFeedResponse; +import com.moplus.moplus_server.client.homefeed.dto.response.HomeFeedResponse.DailyProgressResponse; +import com.moplus.moplus_server.client.homefeed.dto.response.HomeFeedResponse.ProblemSetHomeFeedResponse; +import com.moplus.moplus_server.client.submit.domain.ProgressStatus; +import com.moplus.moplus_server.client.submit.service.ProblemSubmitGetService; +import com.moplus.moplus_server.domain.problemset.service.ProblemSetGetService; +import com.moplus.moplus_server.member.domain.Member; +import com.moplus.moplus_server.statistic.Problem.domain.ProblemSetStatistic; +import com.moplus.moplus_server.statistic.Problem.repository.ProblemSetStatisticRepository; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.ArrayList; +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 HomeFeedFacadeService { + + private static final LocalDate today = LocalDate.now(); + private static final LocalDate monday = today.with(DayOfWeek.MONDAY); + private static final LocalDate friday = today.with(DayOfWeek.FRIDAY); + private final ProblemSetStatisticRepository problemSetStatisticRepository; + private final PublishGetService publishGetService; + private final ProblemSetGetService problemSetGetService; + private final ProblemSubmitGetService problemSubmitGetService; + + @Transactional(readOnly = true) + public HomeFeedResponse getHomeFeed(Member member) { + Long memberId = member.getId(); + + List publishes = publishGetService.getPublishesBetweenDates(monday, friday); + + List dailyProgresses = getDailyProgresses(memberId, publishes); + List problemSets = getWeekdayProblemSets(publishes); + + return HomeFeedResponse.of(dailyProgresses, problemSets); + } + + private List getDailyProgresses(Long memberId, List publishes) { + Map progressStatuses = problemSubmitGetService.getProgressStatuses(memberId, + publishes); + + List responses = new ArrayList<>(); + for (LocalDate date = monday; !date.isAfter(friday); date = date.plusDays(1)) { + ProgressStatus status = progressStatuses.getOrDefault(date, ProgressStatus.NOT_STARTED); + responses.add(DailyProgressResponse.of(date, status)); + } + + return responses; + } + + private List getWeekdayProblemSets(List publishes) { + + Map publishByDate = publishes.stream() + .collect(Collectors.toMap(Publish::getPublishedDate, publish -> publish)); + + // 문제 세트 정보 조회 + List problemSetIds = publishes.stream() + .map(Publish::getProblemSetId) + .toList(); + Map problemSetMap = problemSetGetService.getProblemSets(problemSetIds).stream() + .collect(Collectors.toMap(ProblemSetGetResponse::id, response -> response)); + + // 월요일부터 금요일까지의 모든 날짜에 대한 응답 생성 + List responses = new ArrayList<>(); + for (LocalDate date = monday; !date.isAfter(friday); date = date.plusDays(1)) { + Publish publish = publishByDate.get(date); + if (publish != null) { + ProblemSetGetResponse problemSet = problemSetMap.get(publish.getProblemSetId()); + Long submitCount = problemSetStatisticRepository.findById(problemSet.id()) + .map(ProblemSetStatistic::getSubmitCount) + .orElse(0L); + responses.add(ProblemSetHomeFeedResponse.of(date, publish.getId(), problemSet, submitCount)); + } else { + responses.add(ProblemSetHomeFeedResponse.of(date)); + } + } + + return responses; + } +} \ No newline at end of file diff --git a/src/main/java/com/moplus/moplus_server/client/problem/controller/ProblemGetController.java b/src/main/java/com/moplus/moplus_server/client/problem/controller/ProblemGetController.java new file mode 100644 index 00000000..a38d9940 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/problem/controller/ProblemGetController.java @@ -0,0 +1,88 @@ +package com.moplus.moplus_server.client.problem.controller; + +import com.moplus.moplus_server.client.problem.dto.response.AllProblemGetResponse; +import com.moplus.moplus_server.client.problem.dto.response.ChildProblemClientGetResponse; +import com.moplus.moplus_server.client.problem.dto.response.ChildProblemsClientGetResponse; +import com.moplus.moplus_server.client.problem.dto.response.ProblemClientGetResponse; +import com.moplus.moplus_server.client.problem.dto.response.ProblemClientThumbnailResponse; +import com.moplus.moplus_server.client.problem.dto.response.PublishClientGetResponse; +import com.moplus.moplus_server.client.problem.service.ProblemsGetService; +import com.moplus.moplus_server.global.annotation.AuthUser; +import com.moplus.moplus_server.member.domain.Member; +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.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "클라이언트 문제 조회", description = "클라이언트 문제 조회 관련 API") +@RestController +@RequestMapping("/api/v1/client") +@RequiredArgsConstructor +public class ProblemGetController { + + private final ProblemsGetService problemsGetService; + + @GetMapping("problem/all/{year}/{month}") + @Operation(summary = "전체 문제 조회", description = "월별 문제들에 대한 진행도와 정보들을 조회합니다.") + public ResponseEntity> getAllProblem( + @PathVariable("year") int year, + @PathVariable("month") int month, + @AuthUser Member member + ) { + return ResponseEntity.ok(problemsGetService.getAllProblem(member.getId(), year, month)); + } + + @GetMapping("problem/{publishId}") + @Operation(summary = "특정 발행 속 문항들 조회", description = "사용자에게 보여지는 특정 발행에 속한 문항을 조회합니다.") + public ResponseEntity getProblemsInPublish( + @PathVariable("publishId") Long publishId, + @AuthUser Member member + ) { + return ResponseEntity.ok(problemsGetService.getProblemsInPublish(member.getId(), publishId)); + } + + @GetMapping("problem/{publishId}/{problemId}") + @Operation(summary = "문항 조회", description = "사용자에게 보여지는 문항을 조회합니다.") + public ResponseEntity getProblem( + @PathVariable("publishId") Long publishId, + @PathVariable("problemId") Long problemId, + @AuthUser Member member + ) { + return ResponseEntity.ok(problemsGetService.getProblem(member.getId(), publishId, problemId)); + } + + @GetMapping("problem/{publishId}/{problemId}/{childProblemId}") + @Operation(summary = "새끼문항 조회", description = "사용자에게 보여지는 새끼문항을 조회합니다.") + public ResponseEntity getChildProblem( + @PathVariable("publishId") Long publishId, + @PathVariable("problemId") Long problemId, + @PathVariable("childProblemId") Long childProblemId, + @AuthUser Member member + ) { + return ResponseEntity.ok( + problemsGetService.getChildProblem(member.getId(), publishId, problemId, childProblemId)); + } + + @GetMapping("problem/thumbnail/{publishId}/{problemId}") + @Operation(summary = "문항 썸네일 조회", description = "바로 풀어보기/단계별로 풀어보기 화면에서 필요한 문항을 조회합니다.") + public ResponseEntity getProblemThumbnail( + @PathVariable Long publishId, + @PathVariable Long problemId + ) { + return ResponseEntity.ok(problemsGetService.getProblemThumbnail(publishId, problemId)); + } + + @GetMapping("problem/child/{publishId}/{problemId}") + @Operation(summary = "문항에 포함된 새끼문항 정보 조회", description = "단계별로 풀어보기 이후 화면들에서 필요한 정보들을 조회합니다.") + public ResponseEntity getChildProblems( + @PathVariable Long publishId, + @PathVariable Long problemId + ) { + return ResponseEntity.ok(problemsGetService.getChildProblems(publishId, problemId)); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/problem/dto/response/AllProblemGetResponse.java b/src/main/java/com/moplus/moplus_server/client/problem/dto/response/AllProblemGetResponse.java new file mode 100644 index 00000000..5ae37bea --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/problem/dto/response/AllProblemGetResponse.java @@ -0,0 +1,27 @@ +package com.moplus.moplus_server.client.problem.dto.response; + +import com.moplus.moplus_server.client.submit.domain.ProblemSubmitStatus; +import com.moplus.moplus_server.client.submit.dto.response.DayProgress; +import java.time.LocalDate; +import java.util.List; +import lombok.Builder; + +@Builder +public record AllProblemGetResponse( + Long publishId, + LocalDate date, + DayProgress progress, + List problemStatuses, + String mainProblemImageUrl +) { + public static AllProblemGetResponse of(Long publishId, LocalDate date, DayProgress progress, + List problemStatuses, String mainProblemImageUrl) { + return AllProblemGetResponse.builder() + .publishId(publishId) + .date(date) + .progress(progress) + .problemStatuses(problemStatuses) + .mainProblemImageUrl(mainProblemImageUrl) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ChildProblemClientGetResponse.java b/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ChildProblemClientGetResponse.java new file mode 100644 index 00000000..64299723 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ChildProblemClientGetResponse.java @@ -0,0 +1,38 @@ +package com.moplus.moplus_server.client.problem.dto.response; + +import com.moplus.moplus_server.client.submit.domain.ChildProblemSubmitStatus; +import com.moplus.moplus_server.client.submit.domain.ProblemSubmitStatus; +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 lombok.Builder; + +@Builder +public record ChildProblemClientGetResponse( + @NotNull(message = "문항번호는 필수입니다.") + int problemNumber, + @NotNull(message = "새끼문항번호는 필수입니다.") + int childProblemNumber, + @NotNull(message = "이미지URL은 필수입니다.") + String imageUrl, + @NotNull(message = "새끼문항 제출상태는 필수입니다.") + ChildProblemSubmitStatus status, + @NotNull(message = "답변타입은 필수입니다.") + AnswerType answerType, + @NotNull(message = "정답은 필수입니다.") + String answer +) { + public static ChildProblemClientGetResponse of(int problemNumber, int childProblemNumber, ChildProblem childProblem, + ChildProblemSubmitStatus status + ) { + return ChildProblemClientGetResponse.builder() + .problemNumber(problemNumber) + .childProblemNumber(childProblemNumber) + .imageUrl(childProblem.getImageUrl()) + .status(status) + .answerType(childProblem.getAnswerType()) + .answer(status == ChildProblemSubmitStatus.CORRECT || status == ChildProblemSubmitStatus.RETRY_CORRECT + ? childProblem.getAnswer() : null) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ChildProblemsClientGetResponse.java b/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ChildProblemsClientGetResponse.java new file mode 100644 index 00000000..d244ac83 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ChildProblemsClientGetResponse.java @@ -0,0 +1,22 @@ +package com.moplus.moplus_server.client.problem.dto.response; + +import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem; +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 ChildProblemsClientGetResponse( + @NotNull(message = "메인문항 이미지URL은 필수입니다.") + String mainProblemImageUrl, + @NotNull(message = "새끼문항ID 리스트는 필수입니다.") + List childProblemIds +) { + public static ChildProblemsClientGetResponse of(Problem problem) { + return ChildProblemsClientGetResponse.builder() + .mainProblemImageUrl(problem.getMainProblemImageUrl()) + .childProblemIds(problem.getChildProblems().stream().map(ChildProblem::getId).toList()) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ProblemClientGetResponse.java b/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ProblemClientGetResponse.java new file mode 100644 index 00000000..c33f3255 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ProblemClientGetResponse.java @@ -0,0 +1,45 @@ +package com.moplus.moplus_server.client.problem.dto.response; + +import com.moplus.moplus_server.client.submit.domain.ChildProblemSubmitStatus; +import com.moplus.moplus_server.client.submit.domain.ProblemSubmitStatus; +import com.moplus.moplus_server.domain.problem.domain.Answer; +import com.moplus.moplus_server.domain.problem.domain.problem.AnswerType; +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 ProblemClientGetResponse( + @NotNull(message = "문항번호는 필수입니다.") + int number, + @NotNull(message = "문항이미지는 필수입니다.") + String imageUrl, + @NotNull(message = "추천시간은 필수입니다.") + Integer recommendedMinute, + @NotNull(message = "추천시간은 필수입니다.") + Integer recommendedSecond, + @NotNull(message = "문항제출 상태는 필수입니다.") + ProblemSubmitStatus status, + @NotNull(message = "새끼문항제출 상태는 필수입니다.") + List childProblemStatuses, + @NotNull(message = "답변타입은 필수입니다.") + AnswerType answerType, + @NotNull(message = "정답은 필수입니다.") + String answer +) { + public static ProblemClientGetResponse of(Problem problem, ProblemSubmitStatus status, + List childProblemStatuses, int number) { + return ProblemClientGetResponse.builder() + .number(number) + .imageUrl(problem.getMainProblemImageUrl()) + .status(status) + .childProblemStatuses(childProblemStatuses) + .recommendedMinute(problem.getRecommendedTime().getMinute()) + .recommendedSecond(problem.getRecommendedTime().getSecond()) + .answerType(problem.getAnswerType()) + .answer(status == ProblemSubmitStatus.CORRECT || status == ProblemSubmitStatus.RETRY_CORRECT + ? problem.getAnswer() : null) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ProblemClientThumbnailResponse.java b/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ProblemClientThumbnailResponse.java new file mode 100644 index 00000000..6c429dba --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ProblemClientThumbnailResponse.java @@ -0,0 +1,21 @@ +package com.moplus.moplus_server.client.problem.dto.response; + +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; +import lombok.Builder; + +@Builder +public record ProblemClientThumbnailResponse( + int number, + String imageUrl, + Integer recommendedMinute, + Integer recommendedSecond +) { + public static ProblemClientThumbnailResponse of(int number, Problem problem) { + return ProblemClientThumbnailResponse.builder() + .number(number) + .imageUrl(problem.getMainProblemImageUrl()) + .recommendedMinute(problem.getRecommendedTime().getMinute()) + .recommendedSecond(problem.getRecommendedTime().getSecond()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ProblemFeedProgressesGetResponse.java b/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ProblemFeedProgressesGetResponse.java new file mode 100644 index 00000000..1b8511a3 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/problem/dto/response/ProblemFeedProgressesGetResponse.java @@ -0,0 +1,22 @@ +package com.moplus.moplus_server.client.problem.dto.response; + +import com.moplus.moplus_server.client.submit.domain.ChildProblemSubmitStatus; +import com.moplus.moplus_server.client.submit.domain.ProblemSubmitStatus; +import java.util.List; +import lombok.Builder; + +@Builder +public record ProblemFeedProgressesGetResponse( + Long problemId, + ProblemSubmitStatus status, + List childProblemStatuses +) { + public static ProblemFeedProgressesGetResponse of(ProblemSubmitStatus status, + List childProblemStatuses, Long problemId) { + return ProblemFeedProgressesGetResponse.builder() + .problemId(problemId) + .status(status) + .childProblemStatuses(childProblemStatuses) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/problem/dto/response/PublishClientGetResponse.java b/src/main/java/com/moplus/moplus_server/client/problem/dto/response/PublishClientGetResponse.java new file mode 100644 index 00000000..d0ba4f4e --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/problem/dto/response/PublishClientGetResponse.java @@ -0,0 +1,26 @@ +package com.moplus.moplus_server.client.problem.dto.response; + +import com.moplus.moplus_server.admin.publish.domain.Publish; +import com.moplus.moplus_server.domain.problemset.domain.ProblemSet; +import java.time.LocalDate; +import java.util.List; +import lombok.Builder; + +@Builder +public record PublishClientGetResponse( + Long publishId, + LocalDate date, + String title, + List problems +) { + + public static PublishClientGetResponse of(Publish publish, ProblemSet problemSet, + List problems) { + return PublishClientGetResponse.builder() + .publishId(publish.getId()) + .date(publish.getPublishedDate()) + .title(problemSet.getTitle().getValue()) + .problems(problems) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/problem/service/ProblemsGetService.java b/src/main/java/com/moplus/moplus_server/client/problem/service/ProblemsGetService.java new file mode 100644 index 00000000..27c16e19 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/problem/service/ProblemsGetService.java @@ -0,0 +1,260 @@ +package com.moplus.moplus_server.client.problem.service; + + +import com.moplus.moplus_server.admin.publish.domain.Publish; +import com.moplus.moplus_server.client.problem.dto.response.AllProblemGetResponse; +import com.moplus.moplus_server.client.problem.dto.response.ChildProblemClientGetResponse; +import com.moplus.moplus_server.client.problem.dto.response.ChildProblemsClientGetResponse; +import com.moplus.moplus_server.client.problem.dto.response.ProblemClientGetResponse; +import com.moplus.moplus_server.client.problem.dto.response.ProblemFeedProgressesGetResponse; +import com.moplus.moplus_server.client.problem.dto.response.ProblemClientThumbnailResponse; +import com.moplus.moplus_server.client.problem.dto.response.PublishClientGetResponse; +import com.moplus.moplus_server.client.submit.domain.ChildProblemSubmit; +import com.moplus.moplus_server.client.submit.domain.ChildProblemSubmitStatus; +import com.moplus.moplus_server.client.submit.domain.ProblemSubmit; +import com.moplus.moplus_server.client.submit.domain.ProblemSubmitStatus; +import com.moplus.moplus_server.client.submit.dto.response.DayProgress; +import com.moplus.moplus_server.client.submit.repository.ChildProblemSubmitRepository; +import com.moplus.moplus_server.client.submit.repository.ProblemSubmitRepository; +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.domain.problemset.domain.ProblemSet; +import com.moplus.moplus_server.domain.problemset.repository.ProblemSetRepository; +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 com.moplus.moplus_server.global.error.exception.NotFoundException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ProblemsGetService { + + private static final int MIN_MONTH = 1; + private static final int MAX_MONTH = 12; + private final PublishRepository publishRepository; + private final ProblemSubmitRepository problemSubmitRepository; + private final ProblemRepository problemRepository; + private final ProblemSetRepository problemSetRepository; + private final ChildProblemSubmitRepository childProblemSubmitRepository; + private final ChildProblemRepository childProblemRepository; + + @Transactional(readOnly = true) + public PublishClientGetResponse getProblemsInPublish(Long memberId, Long publishId) { + Publish publish = publishRepository.findByIdElseThrow(publishId); + denyAccessToFuturePublish(publish); + + ProblemSet problemSet = problemSetRepository.findByIdElseThrow(publish.getProblemSetId()); + List problemIds = problemSet.getProblemIds(); + List problems = problemRepository.findAllById(problemIds); + + List problemClientGetResponses = problems.stream() + .map(problem -> getProblemStatus(memberId, publishId, problem.getId())) + .toList(); + + return PublishClientGetResponse.of(publish, problemSet, problemClientGetResponses); + } + + @Transactional(readOnly = true) + public List getAllProblem(Long memberId, int year, int month) { + + if (month < MIN_MONTH || month > MAX_MONTH) { + 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).stream() + .filter(publish -> !publish.getPublishedDate().isAfter(LocalDate.now())) + .toList(); + + List result = new ArrayList<>(); + + for (Publish publish : publishes) { + Long publishId = publish.getId(); + LocalDate date = publish.getPublishedDate(); + + // 날짜별 사용자 제출 정보 조회 + List submissions = problemSubmitRepository.findByMemberIdAndPublishId(memberId, publishId); + List problemStatuses = submissions.stream() + .map(ProblemSubmit::getStatus) + .toList(); + + // 사용자 제출 정보 바탕으로 진행도 결정 + DayProgress progress = DayProgress.determineDayProgress(problemStatuses); + String mainProblemImageUrl = getMainProblemImageUrl(publish.getProblemSetId()); + + result.add(AllProblemGetResponse.of(publishId, date, progress, problemStatuses, mainProblemImageUrl)); + } + return result; + } + + private String getMainProblemImageUrl(Long problemSetId) { + return problemSetRepository.findById(problemSetId) + .flatMap(problemSet -> problemSet.getProblemIds().stream().findFirst()) + .flatMap(problemRepository::findById) + .map(Problem::getMainProblemImageUrl) + .orElse(null); + } + + @Transactional(readOnly = true) + public ProblemClientGetResponse getProblem(Long memberId, Long publishId, Long problemId) { + + // 발행 조회 + Publish publish = publishRepository.findByIdElseThrow(publishId); + denyAccessToFuturePublish(publish); + + // 문항 번호 추출 + ProblemSet problemSet = problemSetRepository.findByIdElseThrow(publish.getProblemSetId()); + List problemIds = problemSet.getProblemIds(); + int number = problemIds.indexOf(problemId); + if (number == -1) { + throw new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND_IN_PROBLEM_SET); + } + + // 문항조회 + Problem problem = problemRepository.findByIdElseThrow(problemId); + + // 문항 제출 조회 + ProblemSubmit problemSubmit = problemSubmitRepository.findByMemberIdAndPublishIdAndProblemIdElseThrow(memberId, + publishId, problemId); + + // 새끼 문항 제출 상태 조회 + List childProblemIds = problem.getChildProblems().stream() + .map(ChildProblem::getId) + .toList(); + + List childProblemStatuses = childProblemSubmitRepository.findAllByMemberIdAndPublishIdAndChildProblemIdIn( + memberId, publishId, childProblemIds).stream() + .map(ChildProblemSubmit::getStatus) + .toList(); + + return ProblemClientGetResponse.of(problem, problemSubmit.getStatus(), childProblemStatuses, number + 1); + } + + @Transactional(readOnly = true) + public ChildProblemClientGetResponse getChildProblem(Long memberId, Long publishId, Long problemId, + Long childProblemId) { + + // 발행 조회 + Publish publish = publishRepository.findByIdElseThrow(publishId); + denyAccessToFuturePublish(publish); + + // 문항/새끼문항 조회 + Problem problem = problemRepository.findByIdElseThrow(problemId); + ChildProblem childProblem = childProblemRepository.findByIdElseThrow(childProblemId); + + // 새끼문항 제출 조회 + ChildProblemSubmit childProblemSubmit = childProblemSubmitRepository.findByMemberIdAndPublishIdAndChildProblemIdElseThrow( + memberId, publishId, childProblemId); + + int childProblemNumber = 0; + for (int i = 0; i < problem.getChildProblems().size(); i++) { + if (problem.getChildProblems().get(i).getId().equals(childProblemId)) { + childProblemNumber = i + 1; + break; + } + } + + // 문항 번호 추출 + ProblemSet problemSet = problemSetRepository.findByIdElseThrow(publish.getProblemSetId()); + List problemIds = problemSet.getProblemIds(); + int problemNumber = problemIds.indexOf(problemId); + if (problemNumber == -1) { + throw new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND_IN_PROBLEM_SET); + } + + return ChildProblemClientGetResponse.of(problemNumber + 1, childProblemNumber, childProblem, + childProblemSubmit.getStatus()); + } + + private void denyAccessToFuturePublish(Publish publish) { + if (publish.getPublishedDate().isAfter(LocalDate.now())) { + throw new InvalidValueException(ErrorCode.FUTURE_PUBLISH_NOT_ACCESSIBLE); + } + } + + private ProblemFeedProgressesGetResponse getProblemStatus(Long memberId, Long publishId, Long problemId) { + // 문항 조회 + Problem problem = problemRepository.findByIdElseThrow(problemId); + + // 문항 제출 상태 조회 (없으면 NOT_STARTED) + ProblemSubmitStatus problemStatus = problemSubmitRepository + .findByMemberIdAndPublishIdAndProblemId(memberId, publishId, problemId) + .map(ProblemSubmit::getStatus) + .orElse(ProblemSubmitStatus.NOT_STARTED); + + // 새끼 문항 제출 상태 조회 + List childProblemIds = problem.getChildProblems().stream() + .map(ChildProblem::getId) + .toList(); + + List childProblemStatuses = new ArrayList<>(childProblemSubmitRepository + .findAllByMemberIdAndPublishIdAndChildProblemIdIn(memberId, publishId, childProblemIds) + .stream() + .map(ChildProblemSubmit::getStatus) + .toList()); + + // 새끼 문항이 있는데 제출 상태가 없는 경우 NOT_STARTED 추가 + if (childProblemStatuses.size() < childProblemIds.size()) { + int remainingCount = childProblemIds.size() - childProblemStatuses.size(); + for (int i = 0; i < remainingCount; i++) { + childProblemStatuses.add(ChildProblemSubmitStatus.NOT_STARTED); + } + } + + return ProblemFeedProgressesGetResponse.of(problemStatus, childProblemStatuses, problemId); + } + + @Transactional(readOnly = true) + public ProblemClientThumbnailResponse getProblemThumbnail(Long publishId, Long problemId) { + // 발행 조회 + Publish publish = publishRepository.findByIdElseThrow(publishId); + denyAccessToFuturePublish(publish); + + // 문항 세트 조회 + ProblemSet problemSet = problemSetRepository.findByIdElseThrow(publish.getProblemSetId()); + List problemIds = problemSet.getProblemIds(); + + // 문항 번호 추출 + int problemNumber = problemIds.indexOf(problemId); + if (problemNumber == -1) { + throw new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND_IN_PROBLEM_SET); + } + + //문항 조회 + Problem problem = problemRepository.findByIdElseThrow(problemId); + return ProblemClientThumbnailResponse.of(problemNumber + 1, problem); + } + + @Transactional(readOnly = true) + public ChildProblemsClientGetResponse getChildProblems(Long publishId, Long problemId) { + // 발행 조회 + Publish publish = publishRepository.findByIdElseThrow(publishId); + denyAccessToFuturePublish(publish); + + // 문항 세트 조회 + ProblemSet problemSet = problemSetRepository.findByIdElseThrow(publish.getProblemSetId()); + List problemIds = problemSet.getProblemIds(); + + // 발행된 문항세트에 포함된 문항인지 검사 + int problemNumber = problemIds.indexOf(problemId); + if (problemNumber == -1) { + throw new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND_IN_PROBLEM_SET); + } + + //문항 조회 + Problem problem = problemRepository.findByIdElseThrow(problemId); + return ChildProblemsClientGetResponse.of(problem); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/controller/.gitkeep b/src/main/java/com/moplus/moplus_server/client/submit/controller/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/moplus/moplus_server/client/submit/controller/ClientSubmitController.java b/src/main/java/com/moplus/moplus_server/client/submit/controller/ClientSubmitController.java new file mode 100644 index 00000000..e90d4292 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/controller/ClientSubmitController.java @@ -0,0 +1,79 @@ +package com.moplus.moplus_server.client.submit.controller; + +import com.moplus.moplus_server.client.submit.domain.ChildProblemSubmitStatus; +import com.moplus.moplus_server.client.submit.domain.ProblemSubmitStatus; +import com.moplus.moplus_server.client.submit.dto.request.ChildProblemSubmitCreateRequest; +import com.moplus.moplus_server.client.submit.dto.request.ChildProblemSubmitUpdateIncorrectRequest; +import com.moplus.moplus_server.client.submit.dto.request.ChildProblemSubmitUpdateRequest; +import com.moplus.moplus_server.client.submit.dto.request.ProblemSubmitCreateRequest; +import com.moplus.moplus_server.client.submit.dto.request.ProblemSubmitUpdateRequest; +import com.moplus.moplus_server.client.submit.dto.response.ChildProblemSubmitUpdateResponse; +import com.moplus.moplus_server.client.submit.service.ClientSubmitService; +import com.moplus.moplus_server.global.annotation.AuthUser; +import com.moplus.moplus_server.member.domain.Member; +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.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/client") +@RequiredArgsConstructor +public class ClientSubmitController { + + private final ClientSubmitService clientSubmitService; + + @PostMapping("problemSubmit") + @Operation(summary = "문항 제출 생성", description = "문항 제출을 '진행중'으로 생성합니다.") + public ResponseEntity createProblemSubmit( + @RequestBody ProblemSubmitCreateRequest request, + @AuthUser Member member + ) { + clientSubmitService.createProblemSubmit(member.getId(), request); + return ResponseEntity.ok(null); + } + + @PutMapping("problemSubmit") + @Operation(summary = "문항 제출 업데이트", description = "제출한 답안을 바탕으로 문항 제출의 상태를 업데이트합니다.") + public ResponseEntity updateProblemSubmit( + @RequestBody ProblemSubmitUpdateRequest request, + @AuthUser Member member + ) { + return ResponseEntity.ok(clientSubmitService.updateProblemSubmit(member.getId(), request)); + } + + @PostMapping("childProblemSubmit") + @Operation(summary = "새끼문항 제출 생성", description = "문항에 속한 새끼문항들을 '시작전'으로 생성합니다.") + public ResponseEntity createProblemSubmit( + @RequestBody ChildProblemSubmitCreateRequest request, + @AuthUser Member member + ) { + clientSubmitService.createChildProblemSubmit(member.getId(), request); + return ResponseEntity.ok(null); + } + + @PutMapping("childProblemSubmit") + @Operation(summary = "새끼문항 제출 업데이트", description = "제출한 답안을 바탕으로 문항 제출의 상태를 업데이트합니다.") + public ResponseEntity updateChildProblemSubmit( + @RequestBody ChildProblemSubmitUpdateRequest request, + @AuthUser Member member + ) { + return ResponseEntity.ok(clientSubmitService.updateChildProblemSubmit(member.getId(), request)); + } + + @PutMapping("childProblemSubmit/incorrect") + @Operation(summary = "새끼문항 제출 틀림 업데이트", description = "새끼문항 제출의 상태를 틀림으로 업데이트합니다.") + public ResponseEntity updateChildProblemSubmitIncorrect( + @RequestBody ChildProblemSubmitUpdateIncorrectRequest request, + @AuthUser Member member + ) { + clientSubmitService.updateChildProblemSubmitIncorrect(member.getId(), request); + return ResponseEntity.ok(null); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/controller/CommentaryGetController.java b/src/main/java/com/moplus/moplus_server/client/submit/controller/CommentaryGetController.java new file mode 100644 index 00000000..025566c9 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/controller/CommentaryGetController.java @@ -0,0 +1,33 @@ +package com.moplus.moplus_server.client.submit.controller; + +import com.moplus.moplus_server.client.submit.dto.response.CommentaryGetResponse; +import com.moplus.moplus_server.client.submit.service.CommentaryGetService; +import com.moplus.moplus_server.global.annotation.AuthUser; +import com.moplus.moplus_server.member.domain.Member; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "클라이언트 해설 조회", description = "클라이언트 해설 조회 관련 API") +@RestController +@RequestMapping("/api/v1/client") +@RequiredArgsConstructor +public class CommentaryGetController { + + private final CommentaryGetService commentaryGetService; + + @GetMapping("commentary") + @Operation(summary = "해설 조회", description = "문항 별 해설/처방을 조회합니다.") + public ResponseEntity getCommentary( + @RequestParam(value = "publishId", required = false) Long publishId, + @RequestParam(value = "problemId", required = false) Long problemId, + @AuthUser Member member + ) { + return ResponseEntity.ok(commentaryGetService.getCommentary(member.getId(), publishId, problemId)); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/domain/ChildProblemSubmit.java b/src/main/java/com/moplus/moplus_server/client/submit/domain/ChildProblemSubmit.java index c7bb92c6..25a869e8 100644 --- a/src/main/java/com/moplus/moplus_server/client/submit/domain/ChildProblemSubmit.java +++ b/src/main/java/com/moplus/moplus_server/client/submit/domain/ChildProblemSubmit.java @@ -37,4 +37,13 @@ public ChildProblemSubmit(Long memberId, Long publishId, Long childProblemId, Ch this.childProblemId = childProblemId; this.status = status; } + + public void updateStatus(ChildProblemSubmitStatus status) { + this.status = status; + } + + public void updateStatusIncorrect() { + this.status = ChildProblemSubmitStatus.INCORRECT; + + } } diff --git a/src/main/java/com/moplus/moplus_server/client/submit/domain/ChildProblemSubmitStatus.java b/src/main/java/com/moplus/moplus_server/client/submit/domain/ChildProblemSubmitStatus.java index 541c308e..c869d59b 100644 --- a/src/main/java/com/moplus/moplus_server/client/submit/domain/ChildProblemSubmitStatus.java +++ b/src/main/java/com/moplus/moplus_server/client/submit/domain/ChildProblemSubmitStatus.java @@ -3,5 +3,16 @@ public enum ChildProblemSubmitStatus { CORRECT, INCORRECT, - RETRY_CORRECT + RETRY_CORRECT, + NOT_STARTED; + public static ChildProblemSubmitStatus determineStatus(ChildProblemSubmitStatus currentStatus, String memberAnswer, String childProblemAnswer) { + boolean isCorrect = childProblemAnswer.trim().equals(memberAnswer.trim()); + + return switch (currentStatus) { + case CORRECT -> isCorrect ? CORRECT : INCORRECT; + case INCORRECT -> isCorrect ? RETRY_CORRECT : INCORRECT; + case RETRY_CORRECT -> isCorrect ? RETRY_CORRECT : INCORRECT; + default -> isCorrect ? CORRECT : INCORRECT; + }; + } } diff --git a/src/main/java/com/moplus/moplus_server/client/submit/domain/ProblemSubmit.java b/src/main/java/com/moplus/moplus_server/client/submit/domain/ProblemSubmit.java index 63ae280b..02a6be91 100644 --- a/src/main/java/com/moplus/moplus_server/client/submit/domain/ProblemSubmit.java +++ b/src/main/java/com/moplus/moplus_server/client/submit/domain/ProblemSubmit.java @@ -37,4 +37,8 @@ public ProblemSubmit(Long memberId, Long publishId, Long problemId, ProblemSubmi this.problemId = problemId; this.status = status; } + + public void updateStatus(ProblemSubmitStatus status) { + this.status = status; + } } diff --git a/src/main/java/com/moplus/moplus_server/client/submit/domain/ProblemSubmitStatus.java b/src/main/java/com/moplus/moplus_server/client/submit/domain/ProblemSubmitStatus.java index e42b77d1..2ccaadea 100644 --- a/src/main/java/com/moplus/moplus_server/client/submit/domain/ProblemSubmitStatus.java +++ b/src/main/java/com/moplus/moplus_server/client/submit/domain/ProblemSubmitStatus.java @@ -4,5 +4,18 @@ public enum ProblemSubmitStatus { CORRECT, INCORRECT, IN_PROGRESS, - RETRY_CORRECT + RETRY_CORRECT, + NOT_STARTED, + ; + public static ProblemSubmitStatus determineStatus(ProblemSubmitStatus currentStatus, String memberAnswer, String problemAnswer) { + boolean isCorrect = problemAnswer.trim().equals(memberAnswer.trim()); + + return switch (currentStatus) { + case CORRECT -> isCorrect ? CORRECT : INCORRECT; + case INCORRECT -> isCorrect ? RETRY_CORRECT : INCORRECT; + case IN_PROGRESS -> isCorrect ? CORRECT : INCORRECT; + case NOT_STARTED -> isCorrect ? CORRECT : INCORRECT; + default -> isCorrect ? RETRY_CORRECT : INCORRECT; + }; + } } diff --git a/src/main/java/com/moplus/moplus_server/client/submit/domain/ProgressStatus.java b/src/main/java/com/moplus/moplus_server/client/submit/domain/ProgressStatus.java new file mode 100644 index 00000000..163ce374 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/domain/ProgressStatus.java @@ -0,0 +1,7 @@ +package com.moplus.moplus_server.client.submit.domain; + +public enum ProgressStatus { + COMPLETED, + IN_PROGRESS, + NOT_STARTED +} \ No newline at end of file diff --git a/src/main/java/com/moplus/moplus_server/client/submit/dto/.gitkeep b/src/main/java/com/moplus/moplus_server/client/submit/dto/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ChildProblemSubmitCreateRequest.java b/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ChildProblemSubmitCreateRequest.java new file mode 100644 index 00000000..9f42241e --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ChildProblemSubmitCreateRequest.java @@ -0,0 +1,11 @@ +package com.moplus.moplus_server.client.submit.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record ChildProblemSubmitCreateRequest( + @NotNull(message = "발행 ID는 필수입니다.") + Long publishId, + @NotNull(message = "문항 ID는 필수입니다.") + Long problemId +) { +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ChildProblemSubmitUpdateIncorrectRequest.java b/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ChildProblemSubmitUpdateIncorrectRequest.java new file mode 100644 index 00000000..7ad9628c --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ChildProblemSubmitUpdateIncorrectRequest.java @@ -0,0 +1,11 @@ +package com.moplus.moplus_server.client.submit.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record ChildProblemSubmitUpdateIncorrectRequest( + @NotNull(message = "발행 ID는 필수입니다.") + Long publishId, + @NotNull(message = "새끼문항 ID는 필수입니다.") + Long childProblemId +) { +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ChildProblemSubmitUpdateRequest.java b/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ChildProblemSubmitUpdateRequest.java new file mode 100644 index 00000000..23a452da --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ChildProblemSubmitUpdateRequest.java @@ -0,0 +1,12 @@ +package com.moplus.moplus_server.client.submit.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record ChildProblemSubmitUpdateRequest( + @NotNull(message = "발행 ID는 필수입니다.") + Long publishId, + @NotNull(message = "새끼문항 ID는 필수입니다.") + Long childProblemId, + String answer +) { +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ProblemSubmitCreateRequest.java b/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ProblemSubmitCreateRequest.java new file mode 100644 index 00000000..7aec4f0c --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ProblemSubmitCreateRequest.java @@ -0,0 +1,21 @@ +package com.moplus.moplus_server.client.submit.dto.request; + +import com.moplus.moplus_server.client.submit.domain.ProblemSubmit; +import com.moplus.moplus_server.client.submit.domain.ProblemSubmitStatus; +import jakarta.validation.constraints.NotNull; + +public record ProblemSubmitCreateRequest ( + @NotNull(message = "발행 ID는 필수입니다.") + Long publishId, + @NotNull(message = "문항 ID는 필수입니다.") + Long problemId +){ + public ProblemSubmit toEntity(Long memberId) { + return ProblemSubmit.builder() + .memberId(memberId) + .publishId(this.publishId) + .problemId(this.problemId) + .status(ProblemSubmitStatus.IN_PROGRESS) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ProblemSubmitUpdateRequest.java b/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ProblemSubmitUpdateRequest.java new file mode 100644 index 00000000..1b52175b --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/dto/request/ProblemSubmitUpdateRequest.java @@ -0,0 +1,12 @@ +package com.moplus.moplus_server.client.submit.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record ProblemSubmitUpdateRequest( + @NotNull(message = "발행 ID는 필수입니다.") + Long publishId, + @NotNull(message = "문항 ID는 필수입니다.") + Long problemId, + String answer +) { +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/dto/response/ChildProblemDetailResponse.java b/src/main/java/com/moplus/moplus_server/client/submit/dto/response/ChildProblemDetailResponse.java new file mode 100644 index 00000000..8250034d --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/dto/response/ChildProblemDetailResponse.java @@ -0,0 +1,20 @@ +package com.moplus.moplus_server.client.submit.dto.response; + +import com.moplus.moplus_server.client.submit.domain.ChildProblemSubmitStatus; +import java.util.List; +import lombok.Builder; + +@Builder +public record ChildProblemDetailResponse( + String imageUrl, + List prescriptionImageUrls, + ChildProblemSubmitStatus submitStatus +) { + public static ChildProblemDetailResponse of(String imageUrl, List prescriptionImageUrls, ChildProblemSubmitStatus submitStatus) { + return ChildProblemDetailResponse.builder() + .imageUrl(imageUrl) + .prescriptionImageUrls(prescriptionImageUrls) + .submitStatus(submitStatus) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/dto/response/ChildProblemSubmitUpdateResponse.java b/src/main/java/com/moplus/moplus_server/client/submit/dto/response/ChildProblemSubmitUpdateResponse.java new file mode 100644 index 00000000..5f88cb6b --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/dto/response/ChildProblemSubmitUpdateResponse.java @@ -0,0 +1,20 @@ +package com.moplus.moplus_server.client.submit.dto.response; + +import com.moplus.moplus_server.client.submit.domain.ChildProblemSubmitStatus; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; + +@Builder +public record ChildProblemSubmitUpdateResponse( + @NotNull(message = "새끼문항제출 상태는 필수입니다.") + ChildProblemSubmitStatus status, + @NotNull(message = "새끼문항 정답은 필수입니다.") + String answer +) { + public static ChildProblemSubmitUpdateResponse of(ChildProblemSubmitStatus status, String answer) { + return ChildProblemSubmitUpdateResponse.builder() + .status(status) + .answer(answer) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/dto/response/CommentaryGetResponse.java b/src/main/java/com/moplus/moplus_server/client/submit/dto/response/CommentaryGetResponse.java new file mode 100644 index 00000000..9ac9a2db --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/dto/response/CommentaryGetResponse.java @@ -0,0 +1,30 @@ +package com.moplus.moplus_server.client.submit.dto.response; + +import com.moplus.moplus_server.domain.problem.domain.problem.AnswerType; +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; +import lombok.Builder; + +@Builder +public record CommentaryGetResponse( + int problemNumber, + String answer, + String mainAnalysisImageUrl, + String mainHandwritingExplanationImageUrl, + String readingTipImageUrl, + String seniorTipImageUrl, + AnswerType answerType, + PrescriptionResponse prescription +) { + public static CommentaryGetResponse of(int problemNumber, Problem problem, PrescriptionResponse prescription) { + return CommentaryGetResponse.builder() + .problemNumber(problemNumber) + .answer(problem.getAnswer()) + .mainAnalysisImageUrl(problem.getMainAnalysisImageUrl()) + .mainHandwritingExplanationImageUrl(problem.getMainHandwritingExplanationImageUrl()) + .readingTipImageUrl(problem.getReadingTipImageUrl()) + .seniorTipImageUrl(problem.getSeniorTipImageUrl()) + .answerType(problem.getAnswerType()) + .prescription(prescription) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/dto/response/DayProgress.java b/src/main/java/com/moplus/moplus_server/client/submit/dto/response/DayProgress.java new file mode 100644 index 00000000..5e9251d3 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/dto/response/DayProgress.java @@ -0,0 +1,21 @@ +package com.moplus.moplus_server.client.submit.dto.response; + +import com.moplus.moplus_server.client.submit.domain.ProblemSubmitStatus; +import java.util.List; + +public enum DayProgress { + COMPLETE, + INCOMPLETE, + IN_PROGRESS; + public static DayProgress determineDayProgress(List problemStatuses) { + if (problemStatuses.isEmpty()) { + return INCOMPLETE; + } + else if (problemStatuses.contains(ProblemSubmitStatus.IN_PROGRESS)) { + return IN_PROGRESS; + } + else{ + return COMPLETE; + } + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/dto/response/PrescriptionResponse.java b/src/main/java/com/moplus/moplus_server/client/submit/dto/response/PrescriptionResponse.java new file mode 100644 index 00000000..31e5a01b --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/dto/response/PrescriptionResponse.java @@ -0,0 +1,18 @@ +package com.moplus.moplus_server.client.submit.dto.response; + +import java.util.List; +import lombok.Builder; + +@Builder +public record PrescriptionResponse( + List childProblem, + ProblemDetailResponse mainProblem +) { + public static PrescriptionResponse of(List childProblem, + ProblemDetailResponse mainProblem) { + return PrescriptionResponse.builder() + .childProblem(childProblem) + .mainProblem(mainProblem) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/dto/response/ProblemDetailResponse.java b/src/main/java/com/moplus/moplus_server/client/submit/dto/response/ProblemDetailResponse.java new file mode 100644 index 00000000..2b6f0478 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/dto/response/ProblemDetailResponse.java @@ -0,0 +1,21 @@ +package com.moplus.moplus_server.client.submit.dto.response; + +import com.moplus.moplus_server.client.submit.domain.ProblemSubmitStatus; +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; +import java.util.List; +import lombok.Builder; + +@Builder +public record ProblemDetailResponse( + String imageUrl, + List prescriptionImageUrls, + ProblemSubmitStatus submitStatus +) { + public static ProblemDetailResponse of(Problem problem, ProblemSubmitStatus submitStatus) { + return ProblemDetailResponse.builder() + .imageUrl(problem.getMainProblemImageUrl()) + .prescriptionImageUrls(problem.getPrescriptionImageUrls()) + .submitStatus(submitStatus) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/repository/.gitkeep b/src/main/java/com/moplus/moplus_server/client/submit/repository/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/moplus/moplus_server/client/submit/repository/ChildProblemSubmitRepository.java b/src/main/java/com/moplus/moplus_server/client/submit/repository/ChildProblemSubmitRepository.java new file mode 100644 index 00000000..33a5be8a --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/repository/ChildProblemSubmitRepository.java @@ -0,0 +1,20 @@ +package com.moplus.moplus_server.client.submit.repository; + +import com.moplus.moplus_server.client.submit.domain.ChildProblemSubmit; +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.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChildProblemSubmitRepository extends JpaRepository { + Optional findByMemberIdAndPublishIdAndChildProblemId(Long memberId, Long publishId, Long childProblemId); + + default ChildProblemSubmit findByMemberIdAndPublishIdAndChildProblemIdElseThrow(Long memberId, Long publishId, Long childProblemId) { + return findByMemberIdAndPublishIdAndChildProblemId(memberId, publishId, childProblemId).orElseThrow( + () -> new NotFoundException(ErrorCode.CHILD_PROBLEM_SUBMIT_NOT_CONFIRMED)); + } + + List findAllByMemberIdAndPublishIdAndChildProblemIdIn(Long memberId, Long publishId, + List childProblemIds); +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/repository/ProblemSubmitRepository.java b/src/main/java/com/moplus/moplus_server/client/submit/repository/ProblemSubmitRepository.java new file mode 100644 index 00000000..a78f2f6f --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/repository/ProblemSubmitRepository.java @@ -0,0 +1,20 @@ +package com.moplus.moplus_server.client.submit.repository; + +import com.moplus.moplus_server.client.submit.domain.ProblemSubmit; +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.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProblemSubmitRepository extends JpaRepository { + List findByMemberIdAndPublishId(Long memberId, Long publishId); + + Optional findByMemberIdAndPublishIdAndProblemId(Long memberId, Long publishId, Long problemId); + + default ProblemSubmit findByMemberIdAndPublishIdAndProblemIdElseThrow(Long memberId, Long publishId, + Long problemId) { + return findByMemberIdAndPublishIdAndProblemId(memberId, publishId, problemId).orElseThrow( + () -> new NotFoundException(ErrorCode.PROBLEM_SUBMIT_NOT_CONFIRMED)); + } +} \ No newline at end of file diff --git a/src/main/java/com/moplus/moplus_server/client/submit/service/.gitkeep b/src/main/java/com/moplus/moplus_server/client/submit/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/moplus/moplus_server/client/submit/service/ClientSubmitService.java b/src/main/java/com/moplus/moplus_server/client/submit/service/ClientSubmitService.java new file mode 100644 index 00000000..ded486fb --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/service/ClientSubmitService.java @@ -0,0 +1,152 @@ +package com.moplus.moplus_server.client.submit.service; + + +import com.moplus.moplus_server.client.submit.domain.ChildProblemSubmit; +import com.moplus.moplus_server.client.submit.domain.ChildProblemSubmitStatus; +import com.moplus.moplus_server.client.submit.domain.ProblemSubmit; +import com.moplus.moplus_server.client.submit.domain.ProblemSubmitStatus; +import com.moplus.moplus_server.client.submit.dto.request.ChildProblemSubmitCreateRequest; +import com.moplus.moplus_server.client.submit.dto.request.ChildProblemSubmitUpdateIncorrectRequest; +import com.moplus.moplus_server.client.submit.dto.request.ChildProblemSubmitUpdateRequest; +import com.moplus.moplus_server.client.submit.dto.request.ProblemSubmitCreateRequest; +import com.moplus.moplus_server.client.submit.dto.request.ProblemSubmitUpdateRequest; +import com.moplus.moplus_server.client.submit.dto.response.ChildProblemSubmitUpdateResponse; +import com.moplus.moplus_server.client.submit.repository.ChildProblemSubmitRepository; +import com.moplus.moplus_server.client.submit.repository.ProblemSubmitRepository; +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.domain.publish.repository.PublishRepository; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ClientSubmitService { + + private final ProblemSubmitRepository problemSubmitRepository; + private final ProblemRepository problemRepository; + private final ChildProblemSubmitRepository childProblemSubmitRepository; + private final PublishRepository publishRepository; + private final ChildProblemRepository childProblemRepository; + + @Transactional + public void createProblemSubmit(Long memberId, ProblemSubmitCreateRequest request) { + + // 존재하는 발행인지 검증 + publishRepository.existsByIdElseThrow(request.publishId()); + // 존재하는 문항인지 검증 + problemRepository.existsByIdElseThrow(request.problemId()); + + // 제출이력이 없을때만 생성 + Optional existingProblemSubmit = problemSubmitRepository.findByMemberIdAndPublishIdAndProblemId(memberId, + request.publishId(), request.problemId()); + if (existingProblemSubmit.isEmpty()) { + ProblemSubmit problemSubmit = request.toEntity(memberId); + problemSubmitRepository.save(problemSubmit); + } + } + + @Transactional + public ProblemSubmitStatus updateProblemSubmit(Long memberId, ProblemSubmitUpdateRequest request) { + + // 존재하는 발행인지 검증 + publishRepository.existsByIdElseThrow(request.publishId()); + + // 문항 조회 + Problem problem = problemRepository.findByIdElseThrow(request.problemId()); + + //문항 제출 데이터 조회 + ProblemSubmit problemSubmit = problemSubmitRepository.findByMemberIdAndPublishIdAndProblemIdElseThrow(memberId, + request.publishId(), request.problemId()); + // 제출한 답안에 대한 상태 결정 + ProblemSubmitStatus status = ProblemSubmitStatus.determineStatus(problemSubmit.getStatus(), request.answer(), + problem.getAnswer()); + + problemSubmit.updateStatus(status); + return status; + } + + @Transactional + public void createChildProblemSubmit(Long memberId, ChildProblemSubmitCreateRequest request) { + + // 존재하는 발행인지 검증 + publishRepository.existsByIdElseThrow(request.publishId()); + // 존재하는 문항인지 검증 + problemRepository.existsByIdElseThrow(request.problemId()); + + // 문항제출 이력이 없으면 문항제출 생성 + Optional existingProblemSubmit = problemSubmitRepository.findByMemberIdAndPublishIdAndProblemId(memberId, + request.publishId(), request.problemId()); + if (existingProblemSubmit.isEmpty()) { + ProblemSubmit problemSubmit = ProblemSubmit.builder() + .memberId(memberId) + .publishId(request.publishId()) + .problemId(request.problemId()) + .status(ProblemSubmitStatus.IN_PROGRESS) + .build(); + problemSubmitRepository.save(problemSubmit); + } + + // 문항의 새끼문항 조회 + Problem problem = problemRepository.findByIdElseThrow(request.problemId()); + List childProblems = problem.getChildProblems(); + + // 제출이력이 없을떄만 생성 + for (ChildProblem childProblem : childProblems) { + Long childProblemId = childProblem.getId(); + + Optional existingChildProblemSubmit = childProblemSubmitRepository.findByMemberIdAndPublishIdAndChildProblemId(memberId, + request.publishId(), childProblemId); + if (existingChildProblemSubmit.isEmpty()) { + ChildProblemSubmit childProblemSubmit = ChildProblemSubmit.builder() + .memberId(memberId) + .publishId(request.publishId()) + .childProblemId(childProblemId) + .status(ChildProblemSubmitStatus.NOT_STARTED) + .build(); + childProblemSubmitRepository.save(childProblemSubmit); + } + } + } + + @Transactional + public ChildProblemSubmitUpdateResponse updateChildProblemSubmit(Long memberId, ChildProblemSubmitUpdateRequest request) { + + // 존재하는 발행인지 검증 + publishRepository.existsByIdElseThrow(request.publishId()); + + // 새끼문항 조회 + ChildProblem childProblem = childProblemRepository.findByIdElseThrow(request.childProblemId()); + + //새끼문항 제출 데이터 조회 + ChildProblemSubmit childProblemSubmit = childProblemSubmitRepository.findByMemberIdAndPublishIdAndChildProblemIdElseThrow(memberId, + request.publishId(), request.childProblemId()); + // 제출한 답안에 대한 상태 결정 + ChildProblemSubmitStatus status = ChildProblemSubmitStatus.determineStatus(childProblemSubmit.getStatus(), request.answer(), + childProblem.getAnswer()); + + childProblemSubmit.updateStatus(status); + return ChildProblemSubmitUpdateResponse.of(status, childProblem.getAnswer()); + } + + @Transactional + public void updateChildProblemSubmitIncorrect(Long memberId, ChildProblemSubmitUpdateIncorrectRequest request) { + + // 존재하는 발행인지 검증 + publishRepository.existsByIdElseThrow(request.publishId()); + + // 새끼문항 조회 + ChildProblem childProblem = childProblemRepository.findByIdElseThrow(request.childProblemId()); + + //새끼문항 제출 데이터 조회 + ChildProblemSubmit childProblemSubmit = childProblemSubmitRepository.findByMemberIdAndPublishIdAndChildProblemIdElseThrow(memberId, + request.publishId(), request.childProblemId()); + // 틀림 처리 + childProblemSubmit.updateStatusIncorrect(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/service/CommentaryGetService.java b/src/main/java/com/moplus/moplus_server/client/submit/service/CommentaryGetService.java new file mode 100644 index 00000000..1508838d --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/service/CommentaryGetService.java @@ -0,0 +1,94 @@ +package com.moplus.moplus_server.client.submit.service; + +import com.moplus.moplus_server.admin.publish.domain.Publish; +import com.moplus.moplus_server.client.submit.domain.ChildProblemSubmit; +import com.moplus.moplus_server.client.submit.domain.ChildProblemSubmitStatus; +import com.moplus.moplus_server.client.submit.domain.ProblemSubmit; +import com.moplus.moplus_server.client.submit.domain.ProblemSubmitStatus; +import com.moplus.moplus_server.client.submit.dto.response.ChildProblemDetailResponse; +import com.moplus.moplus_server.client.submit.dto.response.CommentaryGetResponse; +import com.moplus.moplus_server.client.submit.dto.response.PrescriptionResponse; +import com.moplus.moplus_server.client.submit.dto.response.ProblemDetailResponse; +import com.moplus.moplus_server.client.submit.repository.ChildProblemSubmitRepository; +import com.moplus.moplus_server.client.submit.repository.ProblemSubmitRepository; +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.repository.ProblemSetRepository; +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 com.moplus.moplus_server.global.error.exception.NotFoundException; +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CommentaryGetService { + + private final PublishRepository publishRepository; + private final ProblemSubmitRepository problemSubmitRepository; + private final ProblemRepository problemRepository; + private final ProblemSetRepository problemSetRepository; + private final ChildProblemSubmitRepository childProblemSubmitRepository; + + @Transactional(readOnly = true) + public CommentaryGetResponse getCommentary(Long memberId, Long publishId, Long problemId) { + + // 발행 조회 + Publish publish = publishRepository.findByIdElseThrow(publishId); + denyAccessToFuturePublish(publish); + + // 문항 제출 조회 + ProblemSubmit problemSubmit = problemSubmitRepository.findByMemberIdAndPublishIdAndProblemIdElseThrow(memberId, + publishId, problemId); + if (problemSubmit.getStatus() == ProblemSubmitStatus.IN_PROGRESS + || problemSubmit.getStatus() == ProblemSubmitStatus.NOT_STARTED) { + throw new InvalidValueException(ErrorCode.PROBLEM_SUBMIT_NOT_COMPLETED); + } + + // 문항 해설 생성 + Problem problem = problemRepository.findByIdElseThrow(problemId); + ProblemDetailResponse mainProblem = ProblemDetailResponse.of( + problem, + problemSubmit.getStatus() + ); + + // 새끼문항 해설 생성 + List childProblem = problem.getChildProblems().stream() + .map(cp -> ChildProblemDetailResponse.of( + cp.getImageUrl(), + cp.getPrescriptionImageUrls(), + childProblemSubmitRepository.findByMemberIdAndPublishIdAndChildProblemId(memberId, publishId, + cp.getId()) + .map(ChildProblemSubmit::getStatus) + .orElse(ChildProblemSubmitStatus.NOT_STARTED) + )).toList(); + + // 처방 정보 생성 + PrescriptionResponse prescription = PrescriptionResponse.of(childProblem, mainProblem); + + // 문항 번호 추출 + ProblemSet problemSet = problemSetRepository.findByIdElseThrow(publish.getProblemSetId()); + List problemIds = problemSet.getProblemIds(); + int number = problemIds.indexOf(problemId); + if (number == -1) { + throw new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND_IN_PROBLEM_SET); + } + + return CommentaryGetResponse.of( + number + 1, + problem, + prescription + ); + } + + private void denyAccessToFuturePublish(Publish publish){ + if (publish.getPublishedDate().isAfter(LocalDate.now())) { + throw new InvalidValueException(ErrorCode.FUTURE_PUBLISH_NOT_ACCESSIBLE); + } + } +} diff --git a/src/main/java/com/moplus/moplus_server/client/submit/service/ProblemSubmitGetService.java b/src/main/java/com/moplus/moplus_server/client/submit/service/ProblemSubmitGetService.java new file mode 100644 index 00000000..9e3e404f --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/client/submit/service/ProblemSubmitGetService.java @@ -0,0 +1,50 @@ +package com.moplus.moplus_server.client.submit.service; + +import com.moplus.moplus_server.admin.publish.domain.Publish; +import com.moplus.moplus_server.client.submit.domain.ProblemSubmit; +import com.moplus.moplus_server.client.submit.domain.ProgressStatus; +import com.moplus.moplus_server.client.submit.repository.ProblemSubmitRepository; +import com.moplus.moplus_server.domain.problemset.domain.ProblemSet; +import com.moplus.moplus_server.domain.problemset.repository.ProblemSetRepository; +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 ProblemSubmitGetService { + + private final ProblemSubmitRepository problemSubmitRepository; + private final ProblemSetRepository problemSetRepository; + + @Transactional(readOnly = true) + public ProgressStatus getProgressStatus(Long memberId, Long publishId) { + List submits = problemSubmitRepository.findByMemberIdAndPublishId(memberId, publishId); + + if (submits.isEmpty()) { + return ProgressStatus.NOT_STARTED; + } + + ProblemSet problemSet = problemSetRepository.findByIdElseThrow(submits.get(0).getProblemId()); + + int totalProblems = problemSet.getProblemIds().size(); + + if (submits.size() == totalProblems) { + return ProgressStatus.COMPLETED; + } + return ProgressStatus.IN_PROGRESS; + } + + @Transactional(readOnly = true) + public Map getProgressStatuses(Long memberId, List publishes) { + return publishes.stream() + .collect(Collectors.toMap( + Publish::getPublishedDate, + publish -> getProgressStatus(memberId, publish.getId()) + )); + } +} \ No newline at end of file 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 index 862b52a3..87bf89ed 100644 --- 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 @@ -2,6 +2,7 @@ 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.LoginResponse; 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; @@ -17,7 +18,9 @@ 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.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -40,6 +43,21 @@ private static String validateRefreshTokenCookie(HttpServletRequest request) { .orElseThrow(() -> new NotFoundException(ErrorCode.BLANK_INPUT_VALUE)); } + @Override + @PostMapping("/oauth/social-login") + public ResponseEntity socialLogin( + @RequestHeader("social_access_token") String accessToken, + @RequestParam("provider") String provider, + HttpServletResponse response + ) { + LoginResponse loginResponse = authService.socialLogin(accessToken, provider); + + Cookie refreshTokenCookie = cookieUtil.createCookie(loginResponse.refreshToken()); + response.addCookie(refreshTokenCookie); + + return ResponseEntity.ok(loginResponse); + } + @Override @PostMapping("/admin/login") public ResponseEntity adminLogin( 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 index 90a26104..632e5e89 100644 --- 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 @@ -2,12 +2,14 @@ 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.LoginResponse; import com.moplus.moplus_server.global.error.ErrorResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; 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.security.SecurityRequirements; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -16,66 +18,109 @@ @Tag(name = "인증", description = "인증 관련 API") public interface AuthControllerDocs { + @SecurityRequirements(value = {}) + @Operation( + summary = "소셜 로그인", + description = "소셜 액세스 토큰으로 로그인하여 자체 액세스 토큰을 발급받고 리프레시 토큰을 쿠키에 설정합니다.", + responses = { + @ApiResponse( + responseCode = "200", + description = "로그인 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = LoginResponse.class) + ), + headers = { + @Header( + name = "Authorization", + description = "발급된 액세스 토큰", + schema = @Schema(type = "string") + ), + @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 socialLogin( + String accessToken, + String provider, + HttpServletResponse response + ); + + @SecurityRequirements(value = {}) @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" + 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 = "401", - description = "인증 실패 (잘못된 이메일 또는 비밀번호)", - content = @Content(schema = @Schema(implementation = ErrorResponse.class)) - ) - } + } ) ResponseEntity adminLogin(AdminLoginRequest request); + @SecurityRequirements(value = {}) @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" + 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)) ) - ) - ), - @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/response/LoginResponse.java b/src/main/java/com/moplus/moplus_server/domain/auth/dto/response/LoginResponse.java new file mode 100644 index 00000000..cc9126de --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/auth/dto/response/LoginResponse.java @@ -0,0 +1,16 @@ +package com.moplus.moplus_server.domain.auth.dto.response; + +import com.moplus.moplus_server.member.domain.Member; + +public record LoginResponse( + Long memberId, + String email, + String accessToken, + String refreshToken +) { + + public static LoginResponse of(Member member, TokenResponse tokenPairResponse) { + return new LoginResponse(member.getId(), member.getOauthInfo().getOauthEmail(), tokenPairResponse.accessToken(), + tokenPairResponse.refreshToken()); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/auth/dto/response/oauth/OauthUserInfoResponse.java b/src/main/java/com/moplus/moplus_server/domain/auth/dto/response/oauth/OauthUserInfoResponse.java new file mode 100644 index 00000000..a17781d5 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/auth/dto/response/oauth/OauthUserInfoResponse.java @@ -0,0 +1,24 @@ +package com.moplus.moplus_server.domain.auth.dto.response.oauth; + +import com.moplus.moplus_server.domain.auth.service.OAuthProvider; +import com.moplus.moplus_server.member.domain.OauthInfo; +import lombok.Builder; + +@Builder +public record OauthUserInfoResponse( + String oauthId, + String email, + String name, + OAuthProvider provider + +) { + + public OauthInfo toEntity() { + return OauthInfo.builder() + .oauthId(this.oauthId()) + .oauthEmail(this.email()) + .name(this.name()) + .oauthProvider(this.provider().name()) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/auth/dto/response/oauth/kakao/KakaoAuthResponse.java b/src/main/java/com/moplus/moplus_server/domain/auth/dto/response/oauth/kakao/KakaoAuthResponse.java new file mode 100644 index 00000000..4267ebf1 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/auth/dto/response/oauth/kakao/KakaoAuthResponse.java @@ -0,0 +1,15 @@ +package com.moplus.moplus_server.domain.auth.dto.response.oauth.kakao; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record KakaoAuthResponse( + Long id, + @JsonProperty("kakao_account") KakaoAccountResponse kakaoAccount, + @JsonProperty("properties") PropertiesResponse properties) { + public record KakaoAccountResponse(String email) { + } + + public record PropertiesResponse( + String nickname, String profile_image, String thumbnail_image) { + } +} 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 index 8d215b12..4036ce9e 100644 --- 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 @@ -1,12 +1,16 @@ package com.moplus.moplus_server.domain.auth.service; +import com.moplus.moplus_server.domain.auth.dto.response.LoginResponse; import com.moplus.moplus_server.domain.auth.dto.response.TokenResponse; -import com.moplus.moplus_server.member.domain.Member; -import com.moplus.moplus_server.member.service.MemberService; +import com.moplus.moplus_server.domain.auth.dto.response.oauth.OauthUserInfoResponse; +import com.moplus.moplus_server.domain.auth.service.kakao.KakaoClient; 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 com.moplus.moplus_server.member.domain.Member; +import com.moplus.moplus_server.member.domain.OauthInfo; +import com.moplus.moplus_server.member.service.MemberService; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.MalformedJwtException; @@ -20,9 +24,30 @@ @RequiredArgsConstructor public class AuthService { + private final KakaoClient kakaoClient; private final JwtUtil jwtUtil; private final MemberService memberService; + @Transactional + public LoginResponse socialLogin(String socialAccessToken, String provider) { + OauthUserInfoResponse oauthUserInfoResponse = getOauthUserInfo(socialAccessToken, OAuthProvider.from(provider)); + OauthInfo oauthInfo = oauthUserInfoResponse.toEntity(); + + Member member = memberService.getMemberByOAuthInfo(oauthInfo); + + String newAccessToken = jwtUtil.generateAccessToken(member); + String newRefreshToken = jwtUtil.generateRefreshToken(member); + + return LoginResponse.of(member, new TokenResponse(newAccessToken, newRefreshToken)); + } + + private OauthUserInfoResponse getOauthUserInfo(String socialAccessToken, OAuthProvider provider) { + return switch (provider) { + case APPLE -> null; //TODO: Apple OAuth + case KAKAO -> kakaoClient.getOauthUserInfo(socialAccessToken); + }; + } + @Transactional public TokenResponse reissueToken(String refreshToken) { if (refreshToken == null) { diff --git a/src/main/java/com/moplus/moplus_server/domain/auth/service/OAuthProvider.java b/src/main/java/com/moplus/moplus_server/domain/auth/service/OAuthProvider.java new file mode 100644 index 00000000..d2733178 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/auth/service/OAuthProvider.java @@ -0,0 +1,23 @@ +package com.moplus.moplus_server.domain.auth.service; + +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import com.moplus.moplus_server.global.error.exception.InvalidValueException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum OAuthProvider { + KAKAO("KAKAO"), + APPLE("APPLE"), + ; + private final String value; + + public static OAuthProvider from(String provider) { + return switch (provider.toUpperCase()) { + case "APPLE" -> APPLE; + case "KAKAO" -> KAKAO; + default -> throw new InvalidValueException(ErrorCode.INVALID_PROVIDER_TYPE); + }; + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/auth/service/kakao/KakaoClient.java b/src/main/java/com/moplus/moplus_server/domain/auth/service/kakao/KakaoClient.java new file mode 100644 index 00000000..6d6f2a69 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/auth/service/kakao/KakaoClient.java @@ -0,0 +1,51 @@ +package com.moplus.moplus_server.domain.auth.service.kakao; + +import com.moplus.moplus_server.domain.auth.dto.response.oauth.OauthUserInfoResponse; +import com.moplus.moplus_server.domain.auth.dto.response.oauth.kakao.KakaoAuthResponse; +import com.moplus.moplus_server.domain.auth.service.OAuthProvider; +import com.moplus.moplus_server.global.error.exception.AccessDeniedException; +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatusCode; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +@Slf4j +@Component +@RequiredArgsConstructor +public class KakaoClient { + + public static final String KAKAO_USER_ME_URL = "https://kapi.kakao.com/v2/user/me"; + public static final String TOKEN_PREFIX = "Bearer "; + private final RestClient restClient; + + public OauthUserInfoResponse getOauthUserInfo(String token) { + KakaoAuthResponse kakaoAuthResponse = + restClient + .get() + .uri(KAKAO_USER_ME_URL) + .header("Authorization", TOKEN_PREFIX + token) + .exchange( + (request, response) -> { + HttpStatusCode statusCode = response.getStatusCode(); + log.info("Received response with status: {}", statusCode); + + if (!response.getStatusCode().is2xxSuccessful()) { + log.error("Kakao API error. Status: {}, Response: {}", + statusCode, response.bodyTo(String.class)); + throw new AccessDeniedException(ErrorCode.KAKAO_TOKEN_CLIENT_FAILED); + } + return Objects.requireNonNull( + response.bodyTo(KakaoAuthResponse.class)); + }); + + return new OauthUserInfoResponse( + kakaoAuthResponse.id().toString(), + kakaoAuthResponse.kakaoAccount().email(), + kakaoAuthResponse.properties().nickname(), + OAuthProvider.KAKAO + ); + } +} 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 index 52bda4f5..df5f090a 100644 --- 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 @@ -24,6 +24,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; @Getter @Entity @@ -34,6 +35,7 @@ public class ChildProblem extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "child_problem_id") Long id; + @BatchSize(size = 100) @ElementCollection @CollectionTable(name = "child_problem_concept", joinColumns = @JoinColumn(name = "child_problem_id")) @Column(name = "concept_tag_id") @@ -91,4 +93,13 @@ public void update(ChildProblem input) { public String getAnswer() { return answer.getValue(); } + + public boolean isValid() { + return imageUrl != null && !imageUrl.isEmpty() + && answer != null && !answer.getValue().isEmpty() + && answerType != null + && conceptTagIds != null && !conceptTagIds.isEmpty() + && prescriptionImageUrls != null && !prescriptionImageUrls.isEmpty() + && prescriptionImageUrls.stream().allMatch(url -> url != null && !url.isEmpty()); + } } 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 index 75678f11..d77d8763 100644 --- 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 @@ -14,6 +14,7 @@ public enum Subject { 미적분("미적분", 30, 100, 3), 기하("기하", 30, 100, 4), 확률과통계("확률과통계", 30, 100, 5), + 고3_공통("고3_공통", 30, 100, 6), ; private final String value; 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 index 2719d2ec..c0cca478 100644 --- 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 @@ -60,6 +60,7 @@ public class Problem extends BaseEntity { @Convert(converter = StringListConverter.class) @Column(columnDefinition = "TEXT") List prescriptionImageUrls; + @ElementCollection @CollectionTable(name = "problem_concept", joinColumns = @JoinColumn(name = "problem_id")) @Column(name = "concept_tag_id") @@ -179,7 +180,8 @@ public boolean isValid() { && conceptTagIds != null && !conceptTagIds.isEmpty() && recommendedTime != null && recommendedTime.getMinute() != null && recommendedTime.getMinute() >= 0 - && recommendedTime.getSecond() != null && recommendedTime.getSecond() >= 0; + && recommendedTime.getSecond() != null && recommendedTime.getSecond() >= 0 + && childProblems.stream().allMatch(ChildProblem::isValid); } public String getTitle() { 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 index e02d1014..baec84ae 100644 --- 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 @@ -13,4 +13,12 @@ public enum ProblemType { private final String name; private final int code; + + public boolean isCreationProblem() { + return this == CREATION_PROBLEM; + } + + public boolean requiresPracticeTest() { + return !isCreationProblem(); + } } 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 index 4529f682..0a959fc9 100644 --- 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 @@ -4,7 +4,10 @@ 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 java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface ProblemRepository extends JpaRepository { @@ -16,13 +19,16 @@ default void existsByIdElseThrow(Long id) { } } - default void existsByProblemAdminIdElseThrow(ProblemCustomId problemCustomId) { - if (!existsByProblemCustomId(problemCustomId)) { - throw new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND); - } - } + @Query("SELECT DISTINCT p FROM Problem p " + + "LEFT JOIN FETCH p.childProblems c " + + "WHERE p.id = :id") + Optional findByIdWithFetchJoin(@Param("id") Long id); default Problem findByIdElseThrow(Long id) { return findById(id).orElseThrow(() -> new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND)); } + + default Problem findByIdWithFetchJoinElseThrow(Long id) { + return findByIdWithFetchJoin(id).orElseThrow(() -> new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND)); + } } 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 index 7cf4753e..5d381d31 100644 --- 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 @@ -17,23 +17,30 @@ public class ChildProblemService { @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(); + Problem problem = findAndValidateProblem(problemId); + ChildProblem newChildProblem = ChildProblem.createEmptyChildProblem(); + problem.addChildProblem(newChildProblem); + problemRepository.flush(); + return getCreatedChildProblemId(problem); } @Transactional public void deleteChildProblem(Long problemId, Long childProblemId) { + Problem problem = findAndValidateProblem(problemId); + problem.deleteChildProblem(childProblemId); + } + + private Problem findAndValidateProblem(Long problemId) { Problem problem = problemRepository.findByIdElseThrow(problemId); if (problem.isConfirmed()) { throw new InvalidValueException(ErrorCode.CHILD_PROBLEM_UPDATE_AFTER_CONFIRMED); } - problem.deleteChildProblem(childProblemId); - problemRepository.save(problem); + return problem; + } + + private Long getCreatedChildProblemId(Problem problem) { + return problem.getChildProblems() + .get(problem.getChildProblems().size() - 1) + .getId(); } } 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 index 762fc2ad..ed3bb707 100644 --- 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 @@ -1,7 +1,7 @@ package com.moplus.moplus_server.domain.problem.service; -import com.moplus.moplus_server.domain.problem.domain.problem.Problem; import com.moplus.moplus_server.admin.problem.dto.response.ProblemGetResponse; +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -15,7 +15,7 @@ public class ProblemGetService { @Transactional(readOnly = true) public ProblemGetResponse getProblem(Long problemId) { - Problem problem = problemRepository.findByIdElseThrow(problemId); + Problem problem = problemRepository.findByIdWithFetchJoinElseThrow(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 index 71dc0fd3..0cbc11d8 100644 --- 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 @@ -25,19 +25,32 @@ public class ProblemSaveService { @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()); + PracticeTestTag practiceTestTag = getPracticeTestTag(request); + ProblemCustomId problemCustomId = createProblemCustomId(request); + Problem problem = createProblem(request, problemCustomId, practiceTestTag); + + return ProblemPostResponse.of(problemRepository.save(problem)); + } - problem = problemMapper.from(request, problemCustomId, practiceTestTag); - } else { - ProblemCustomId problemCustomId = problemAdminIdService.nextId(request.problemType()); - problem = problemMapper.from(request.problemType(), problemCustomId); + private Problem createProblem(ProblemPostRequest request, ProblemCustomId problemCustomId, PracticeTestTag practiceTestTag) { + if (request.problemType().isCreationProblem()) { + return problemMapper.from(request, problemCustomId, practiceTestTag); } + return problemMapper.from(request.problemType(), problemCustomId); + } - return ProblemPostResponse.of(problemRepository.save(problem)); + private ProblemCustomId createProblemCustomId(ProblemPostRequest request) { + if (request.problemType().requiresPracticeTest()) { + PracticeTestTag practiceTestTag = practiceTestRepository.findByIdElseThrow(request.practiceTestId()); + return problemAdminIdService.nextId(request.number(), practiceTestTag, request.problemType()); + } + return problemAdminIdService.nextId(request.problemType()); } + private PracticeTestTag getPracticeTestTag(ProblemPostRequest request) { + if (request.problemType().requiresPracticeTest()) { + return practiceTestRepository.findByIdElseThrow(request.practiceTestId()); + } + return null; + } } 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 index 248bf112..95834546 100644 --- 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 @@ -1,14 +1,13 @@ package com.moplus.moplus_server.domain.problem.service; +import com.moplus.moplus_server.admin.problem.dto.request.ProblemUpdateRequest; +import com.moplus.moplus_server.admin.problem.dto.response.ProblemGetResponse; 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.admin.problem.dto.request.ProblemUpdateRequest; -import com.moplus.moplus_server.admin.problem.dto.response.ProblemGetResponse; -import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType; 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; @@ -33,15 +32,8 @@ public class ProblemUpdateService { public ProblemGetResponse updateProblem(Long problemId, ProblemUpdateRequest request) { conceptTagRepository.existsByIdElseThrow(request.conceptTagIds()); Problem problem = problemRepository.findByIdElseThrow(problemId); - PracticeTestTag practiceTestTag = null; - ProblemCustomId problemCustomId = null; - if (request.problemType() != ProblemType.CREATION_PROBLEM) { - practiceTestTag = practiceTestRepository.findByIdElseThrow(request.practiceTestId()); - problemCustomId = problemAdminIdService.nextId(request.number(), practiceTestTag, - request.problemType()); - } else { - problemCustomId = problemAdminIdService.nextId(request.problemType()); - } + PracticeTestTag practiceTestTag = getPracticeTestTag(request); + ProblemCustomId problemCustomId = createProblemCustomId(request); Problem inputProblem = problemMapper.from(request, problemCustomId, practiceTestTag); problem.update(inputProblem); @@ -57,4 +49,19 @@ private List changeToChildProblems(ProblemUpdateRequest request) { .map(childProblemMapper::from) .toList(); } + + private ProblemCustomId createProblemCustomId(ProblemUpdateRequest request) { + if (request.problemType().requiresPracticeTest()) { + PracticeTestTag practiceTestTag = practiceTestRepository.findByIdElseThrow(request.practiceTestId()); + return problemAdminIdService.nextId(request.number(), practiceTestTag, request.problemType()); + } + return problemAdminIdService.nextId(request.problemType()); + } + + private PracticeTestTag getPracticeTestTag(ProblemUpdateRequest request) { + if (request.problemType().requiresPracticeTest()) { + return practiceTestRepository.findByIdElseThrow(request.practiceTestId()); + } + return null; + } } 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 index 533674a8..b48961e5 100644 --- 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 @@ -16,6 +16,7 @@ default ProblemSet findByIdElseThrow(Long problemSetId) { default void validatePublishableProblemSet(Long problemSetId) { ProblemSet problemSet = findByIdElseThrow(problemSetId); + //이거 soft delete 어노테이션으로 자동화 해야함(리팩토링 필요) if (problemSet.isDeleted()) { throw new InvalidValueException(ErrorCode.PROBLEM_SET_DELETED); } 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 index ef66f469..8d7791b3 100644 --- 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 @@ -1,14 +1,14 @@ package com.moplus.moplus_server.domain.problemset.service; +import com.moplus.moplus_server.admin.problemset.dto.response.ProblemSetGetResponse; +import com.moplus.moplus_server.admin.problemset.dto.response.ProblemSummaryResponse; +import com.moplus.moplus_server.admin.publish.domain.Publish; 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.ProblemRepository; import com.moplus.moplus_server.domain.problemset.domain.ProblemSet; -import com.moplus.moplus_server.admin.problemset.dto.response.ProblemSetGetResponse; -import com.moplus.moplus_server.admin.problemset.dto.response.ProblemSummaryResponse; import com.moplus.moplus_server.domain.problemset.repository.ProblemSetRepository; -import com.moplus.moplus_server.admin.publish.domain.Publish; import com.moplus.moplus_server.domain.publish.repository.PublishRepository; import com.moplus.moplus_server.global.error.exception.BusinessException; import com.moplus.moplus_server.global.error.exception.ErrorCode; @@ -55,4 +55,11 @@ public ProblemSetGetResponse getProblemSet(Long problemSetId) { } return ProblemSetGetResponse.of(problemSet, publishedDates, problemSummaries); } + + @Transactional(readOnly = true) + public List getProblemSets(List problemSetIds) { + return problemSetIds.stream() + .map(this::getProblemSet) + .toList(); + } } 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 index d7422a1b..37dcce37 100644 --- 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 @@ -15,4 +15,10 @@ default Publish findByIdElseThrow(Long publishId) { } List findByProblemSetId(Long problemSetId); + + default void existsByIdElseThrow(Long id) { + if (!existsById(id)) { + throw new NotFoundException(ErrorCode.PUBLISH_NOT_FOUND); + } + } } diff --git a/src/main/java/com/moplus/moplus_server/global/config/WebConfig.java b/src/main/java/com/moplus/moplus_server/global/config/WebConfig.java index 784551f7..89be44fc 100644 --- a/src/main/java/com/moplus/moplus_server/global/config/WebConfig.java +++ b/src/main/java/com/moplus/moplus_server/global/config/WebConfig.java @@ -26,7 +26,16 @@ public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins(corsAllowedOrigins.toArray(new String[0])) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("*") + .allowedHeaders( + "Authorization", + "Content-Type", + "social_access_token", + "X-Requested-With" + ) + .exposedHeaders( + "Authorization", + "Set-Cookie" + ) .allowCredentials(true); } diff --git a/src/main/java/com/moplus/moplus_server/global/config/restClient/RestClientConfig.java b/src/main/java/com/moplus/moplus_server/global/config/restClient/RestClientConfig.java new file mode 100644 index 00000000..6b56edec --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/global/config/restClient/RestClientConfig.java @@ -0,0 +1,22 @@ +package com.moplus.moplus_server.global.config.restClient; + +import java.time.Duration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestClient; + +@Configuration +public class RestClientConfig { + + @Bean + public RestClient restClient() { + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setConnectTimeout(Duration.ofSeconds(10)); + requestFactory.setReadTimeout(Duration.ofSeconds(5)); + + return RestClient.builder() + .requestFactory(requestFactory) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/global/config/security/SecurityConfig.java b/src/main/java/com/moplus/moplus_server/global/config/security/SecurityConfig.java index 8d563b29..eb5817c6 100644 --- a/src/main/java/com/moplus/moplus_server/global/config/security/SecurityConfig.java +++ b/src/main/java/com/moplus/moplus_server/global/config/security/SecurityConfig.java @@ -1,12 +1,12 @@ package com.moplus.moplus_server.global.config.security; -import com.moplus.moplus_server.member.service.MemberService; import com.moplus.moplus_server.global.security.filter.EmailPasswordAuthenticationFilter; import com.moplus.moplus_server.global.security.filter.JwtAuthenticationFilter; import com.moplus.moplus_server.global.security.handler.EmailPasswordSuccessHandler; import com.moplus.moplus_server.global.security.provider.EmailPasswordAuthenticationProvider; import com.moplus.moplus_server.global.security.provider.JwtTokenProvider; import com.moplus.moplus_server.global.security.utils.JwtUtil; +import com.moplus.moplus_server.member.service.MemberService; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -37,7 +37,7 @@ public class SecurityConfig { private final JwtUtil jwtUtil; private String[] allowUrls = {"/", "/favicon.ico", "/swagger-ui/**", "/v3/**", "/actuator/**", - "/api/v1/auth/reissue"}; + "/api/v1/auth/oauth/**", "/api/v1/auth/reissue"}; @Value("${cors-allowed-origins}") private List corsAllowedOrigins; @@ -120,8 +120,17 @@ public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(corsAllowedOrigins); configuration.addAllowedMethod("*"); - configuration.setAllowedHeaders(List.of("*")); // 허용할 헤더 + configuration.setAllowedHeaders(List.of( + "Authorization", + "Content-Type", + "social_access_token", + "X-Requested-With" + )); configuration.setAllowCredentials(true); + configuration.setExposedHeaders(List.of( + "Authorization", + "Set-Cookie" + )); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); // 모든 경로에 적용 diff --git a/src/main/java/com/moplus/moplus_server/global/error/exception/AccessDeniedException.java b/src/main/java/com/moplus/moplus_server/global/error/exception/AccessDeniedException.java new file mode 100644 index 00000000..e778626d --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/global/error/exception/AccessDeniedException.java @@ -0,0 +1,12 @@ +package com.moplus.moplus_server.global.error.exception; + +public class AccessDeniedException extends BusinessException { + + public AccessDeniedException(ErrorCode errorCode) { + super(errorCode); + } + + public AccessDeniedException(String message, ErrorCode errorCode) { + super(message, errorCode); + } +} diff --git a/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java b/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java index 52a7fa02..e14a1d4d 100644 --- a/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java +++ b/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java @@ -23,6 +23,8 @@ public enum ErrorCode { AT_EXPIRED_AND_RT_NOT_FOUND(HttpStatus.UNAUTHORIZED, "AT는 만료되었고 RT는 비어있습니다."), RT_NOT_FOUND(HttpStatus.UNAUTHORIZED, "RT가 비어있습니다"), INVALID_PRINCIPAL(HttpStatus.UNAUTHORIZED, "잘못된 Principal입니다"), + INVALID_PROVIDER_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 제공자 타입입니다"), + KAKAO_TOKEN_CLIENT_FAILED(HttpStatus.UNAUTHORIZED, "카카오 토큰 클라이언트 요청에 실패했습니다"), //모의고사 PRACTICE_TEST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 모의고사를 찾을 수 없습니다"), @@ -34,6 +36,8 @@ public enum ErrorCode { INVALID_SHORT_NUMBER_ANSWER(HttpStatus.BAD_REQUEST, "주관식 문항의 정답은 0~999 사이의 숫자여야 합니다"), INVALID_CONFIRM_PROBLEM(HttpStatus.BAD_REQUEST, "유효하지 않은 문항들 : "), INVALID_DIFFICULTY(HttpStatus.BAD_REQUEST, "난이도는 1~10 사이의 숫자여야 합니다"), + PROBLEM_NOT_FOUND_IN_PROBLEM_SET(HttpStatus.NOT_FOUND, "해당 날짜에 발행된 문항세트에 존재하는 문항이 아닙니다."), + PROBLEM_NUMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "번호에 해당하는 문항을 찾을 수 없습니다."), //새끼 문항 CHILD_PROBLEM_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 새끼 문제를 찾을 수 없습니다"), @@ -76,6 +80,19 @@ public enum ErrorCode { ALREADY_PUBLISHED_ERROR(HttpStatus.BAD_REQUEST, "이미 발행된 문항세트는 컨펌해제할 수 없습니다."), PROBLEM_SET_DELETED(HttpStatus.BAD_REQUEST, "삭제된 문항세트는 발행할 수 없습니다"), PROBLEM_SET_NOT_CONFIRMED(HttpStatus.BAD_REQUEST, "컨펌되지 않은 문항세트는 발행할 수 없습니다"), + + // 통계 + PROBLEM_STATISTIC_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 문항의 통계 정보를 찾을 수 없습니다"), + PROBLEM_SET_STATISTIC_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 문항세트의 통계 정보를 찾을 수 없습니다"), + CHILD_PROBLEM_STATISTIC_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 새끼 문항의 통계 정보를 찾을 수 없습니다"), + FUTURE_PUBLISH_NOT_ACCESSIBLE(HttpStatus.BAD_REQUEST, "오늘 이후의 발행을 조회할 수 없습니다."), + + // 문항 제출 + PROBLEM_SUBMIT_NOT_CONFIRMED(HttpStatus.NOT_FOUND, "문항 제출 정보를 찾을 수 없습니다."), + PROBLEM_SUBMIT_NOT_COMPLETED(HttpStatus.BAD_REQUEST, "문항 제출을 완료해야 해설을 조회할 수 있습니다."), + + // 새끼문항 제출 + CHILD_PROBLEM_SUBMIT_NOT_CONFIRMED(HttpStatus.NOT_FOUND, "새끼문항 제출 정보를 찾을 수 없습니다."), ; diff --git a/src/main/java/com/moplus/moplus_server/member/domain/Member.java b/src/main/java/com/moplus/moplus_server/member/domain/Member.java index 09a00180..e6890cf0 100644 --- a/src/main/java/com/moplus/moplus_server/member/domain/Member.java +++ b/src/main/java/com/moplus/moplus_server/member/domain/Member.java @@ -2,6 +2,7 @@ import com.moplus.moplus_server.global.common.BaseEntity; import jakarta.persistence.Column; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -30,12 +31,23 @@ public class Member extends BaseEntity { @Enumerated(EnumType.STRING) private MemberRole role; + @Embedded + private OauthInfo oauthInfo; + @Builder - public Member(String name, String email, String password, MemberRole role) { + public Member(String name, String email, String password, MemberRole role, OauthInfo oauthInfo) { this.name = name; this.email = email; this.password = password; this.role = role; + this.oauthInfo = oauthInfo; + } + + public static Member createDefaultOAuthMember(OauthInfo oauthInfo) { + return Member.builder() + .role(MemberRole.USER) + .oauthInfo(oauthInfo) + .build(); } public boolean isMatchingPassword(String password) { diff --git a/src/main/java/com/moplus/moplus_server/member/domain/OauthInfo.java b/src/main/java/com/moplus/moplus_server/member/domain/OauthInfo.java new file mode 100644 index 00000000..8bc1037d --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/member/domain/OauthInfo.java @@ -0,0 +1,28 @@ +package com.moplus.moplus_server.member.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class OauthInfo { + + public String oauthId; + private String oauthProvider; + private String oauthEmail; + @Column(name = "oauth_name") + private String name; + + @Builder + public OauthInfo(String oauthId, String oauthProvider, String oauthEmail, String name) { + this.oauthId = oauthId; + this.oauthProvider = oauthProvider; + this.oauthEmail = oauthEmail; + this.name = name; + } +} diff --git a/src/main/java/com/moplus/moplus_server/member/repository/MemberRepository.java b/src/main/java/com/moplus/moplus_server/member/repository/MemberRepository.java index bebed81c..6195a369 100644 --- a/src/main/java/com/moplus/moplus_server/member/repository/MemberRepository.java +++ b/src/main/java/com/moplus/moplus_server/member/repository/MemberRepository.java @@ -1,12 +1,15 @@ package com.moplus.moplus_server.member.repository; -import com.moplus.moplus_server.member.domain.Member; import com.moplus.moplus_server.global.error.exception.ErrorCode; import com.moplus.moplus_server.global.error.exception.NotFoundException; +import com.moplus.moplus_server.member.domain.Member; +import com.moplus.moplus_server.member.domain.OauthInfo; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { + Optional findByOauthInfo(OauthInfo oauthInfo); + Optional findByEmail(String email); default Member findByEmailOrThrow(String email) { diff --git a/src/main/java/com/moplus/moplus_server/member/service/MemberService.java b/src/main/java/com/moplus/moplus_server/member/service/MemberService.java index d045121d..b7c61d46 100644 --- a/src/main/java/com/moplus/moplus_server/member/service/MemberService.java +++ b/src/main/java/com/moplus/moplus_server/member/service/MemberService.java @@ -1,6 +1,7 @@ package com.moplus.moplus_server.member.service; import com.moplus.moplus_server.member.domain.Member; +import com.moplus.moplus_server.member.domain.OauthInfo; import com.moplus.moplus_server.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -12,6 +13,17 @@ public class MemberService { private final MemberRepository memberRepository; + @Transactional + public Member getMemberByOAuthInfo(OauthInfo oauthInfo) { + return memberRepository.findByOauthInfo(oauthInfo) + .orElseGet(() -> createMember(oauthInfo)); + } + + private Member createMember(OauthInfo oauthInfo) { + Member member = Member.createDefaultOAuthMember(oauthInfo); + return memberRepository.save(member); + } + @Transactional(readOnly = true) public Member getMemberByEmail(String email) { return memberRepository.findByEmailOrThrow(email); diff --git a/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/ChildProblemStatistic.java b/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/ChildProblemStatistic.java new file mode 100644 index 00000000..9740c72c --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/ChildProblemStatistic.java @@ -0,0 +1,50 @@ +package com.moplus.moplus_server.statistic.Problem.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +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 ChildProblemStatistic implements StatisticCounter { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "child_problem_statistic_id") + private Long id; + + private Long childProblemId; + + @Embedded + private CountStatistic countStatistic; + + public ChildProblemStatistic(Long childProblemId) { + this.childProblemId = childProblemId; + this.countStatistic = new CountStatistic(); + } + + @Override + public void addViewCount() { + this.countStatistic.addViewCount(); + } + + @Override + public void addSubmitCount() { + this.countStatistic.addSubmitCount(); + } + + public Long getViewCount() { + return this.countStatistic.getViewCount(); + } + + public Long getSubmitCount() { + return this.countStatistic.getSubmitCount(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/CountStatistic.java b/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/CountStatistic.java new file mode 100644 index 00000000..91e1f0c6 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/CountStatistic.java @@ -0,0 +1,26 @@ +package com.moplus.moplus_server.statistic.Problem.domain; + +import jakarta.persistence.Embeddable; +import lombok.Getter; + +@Getter +@Embeddable +public class CountStatistic implements StatisticCounter { + private Long viewCount; + private Long submitCount; + + public CountStatistic() { + this.viewCount = 0L; + this.submitCount = 0L; + } + + @Override + public void addViewCount() { + this.viewCount++; + } + + @Override + public void addSubmitCount() { + this.submitCount++; + } +} diff --git a/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/ProblemSetStatistic.java b/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/ProblemSetStatistic.java new file mode 100644 index 00000000..92db7f42 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/ProblemSetStatistic.java @@ -0,0 +1,50 @@ +package com.moplus.moplus_server.statistic.Problem.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +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 ProblemSetStatistic implements StatisticCounter { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "problem_set_statistic_id") + private Long id; + + private Long problemSetId; + + @Embedded + private CountStatistic countStatistic; + + public ProblemSetStatistic(Long problemSetId) { + this.problemSetId = problemSetId; + this.countStatistic = new CountStatistic(); + } + + @Override + public void addViewCount() { + this.countStatistic.addViewCount(); + } + + @Override + public void addSubmitCount() { + this.countStatistic.addSubmitCount(); + } + + public Long getViewCount() { + return this.countStatistic.getViewCount(); + } + + public Long getSubmitCount() { + return this.countStatistic.getSubmitCount(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/ProblemStatistic.java b/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/ProblemStatistic.java new file mode 100644 index 00000000..0ab5c68e --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/ProblemStatistic.java @@ -0,0 +1,52 @@ +package com.moplus.moplus_server.statistic.Problem.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +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 ProblemStatistic implements StatisticCounter { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "problem_statistic_id") + private Long id; + + private Long problemId; + + @Embedded + private CountStatistic countStatistic; + + @Builder + public ProblemStatistic(Long problemId) { + this.problemId = problemId; + this.countStatistic = new CountStatistic(); + } + + @Override + public void addViewCount() { + this.countStatistic.addViewCount(); + } + + @Override + public void addSubmitCount() { + this.countStatistic.addSubmitCount(); + } + + public Long getViewCount() { + return this.countStatistic.getViewCount(); + } + + public Long getSubmitCount() { + return this.countStatistic.getSubmitCount(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/StatisticCounter.java b/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/StatisticCounter.java new file mode 100644 index 00000000..3c8b309d --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/StatisticCounter.java @@ -0,0 +1,11 @@ +package com.moplus.moplus_server.statistic.Problem.domain; + +public interface StatisticCounter { + void addSubmitCount(); + + void addViewCount(); + + default void updateCount(StatisticFieldType type) { + type.updateCount(this); + } +} diff --git a/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/StatisticEntityTarget.java b/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/StatisticEntityTarget.java new file mode 100644 index 00000000..d66eba95 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/StatisticEntityTarget.java @@ -0,0 +1,5 @@ +package com.moplus.moplus_server.statistic.Problem.domain; + +public enum StatisticEntityTarget { + PROBLEM, PROBLEM_SET, CHILD_PROBLEM +} diff --git a/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/StatisticFieldType.java b/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/StatisticFieldType.java new file mode 100644 index 00000000..771214d7 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/statistic/Problem/domain/StatisticFieldType.java @@ -0,0 +1,16 @@ +package com.moplus.moplus_server.statistic.Problem.domain; + +import java.util.function.Consumer; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum StatisticFieldType { + VIEW(StatisticCounter::addViewCount), + SUBMIT(StatisticCounter::addSubmitCount); + + private final Consumer countUpdater; + + public void updateCount(StatisticCounter counter) { + countUpdater.accept(counter); + } +} \ No newline at end of file diff --git a/src/main/java/com/moplus/moplus_server/statistic/Problem/repository/ChildProblemStatisticRepository.java b/src/main/java/com/moplus/moplus_server/statistic/Problem/repository/ChildProblemStatisticRepository.java new file mode 100644 index 00000000..31f10665 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/statistic/Problem/repository/ChildProblemStatisticRepository.java @@ -0,0 +1,13 @@ +package com.moplus.moplus_server.statistic.Problem.repository; + +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import com.moplus.moplus_server.global.error.exception.NotFoundException; +import com.moplus.moplus_server.statistic.Problem.domain.ChildProblemStatistic; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChildProblemStatisticRepository extends JpaRepository { + default ChildProblemStatistic findByIdElseThrow(Long id) { + return findById(id) + .orElseThrow(() -> new NotFoundException(ErrorCode.CHILD_PROBLEM_STATISTIC_NOT_FOUND)); + } +} \ No newline at end of file diff --git a/src/main/java/com/moplus/moplus_server/statistic/Problem/repository/ProblemSetStatisticRepository.java b/src/main/java/com/moplus/moplus_server/statistic/Problem/repository/ProblemSetStatisticRepository.java new file mode 100644 index 00000000..349d6d6f --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/statistic/Problem/repository/ProblemSetStatisticRepository.java @@ -0,0 +1,13 @@ +package com.moplus.moplus_server.statistic.Problem.repository; + +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import com.moplus.moplus_server.global.error.exception.NotFoundException; +import com.moplus.moplus_server.statistic.Problem.domain.ProblemSetStatistic; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProblemSetStatisticRepository extends JpaRepository { + default ProblemSetStatistic findByIdElseThrow(Long id) { + return findById(id) + .orElseThrow(() -> new NotFoundException(ErrorCode.PROBLEM_SET_STATISTIC_NOT_FOUND)); + } +} \ No newline at end of file diff --git a/src/main/java/com/moplus/moplus_server/statistic/Problem/repository/ProblemStatisticRepository.java b/src/main/java/com/moplus/moplus_server/statistic/Problem/repository/ProblemStatisticRepository.java new file mode 100644 index 00000000..59bbfcc5 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/statistic/Problem/repository/ProblemStatisticRepository.java @@ -0,0 +1,13 @@ +package com.moplus.moplus_server.statistic.Problem.repository; + +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import com.moplus.moplus_server.global.error.exception.NotFoundException; +import com.moplus.moplus_server.statistic.Problem.domain.ProblemStatistic; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProblemStatisticRepository extends JpaRepository { + default ProblemStatistic findByIdElseThrow(Long id) { + return findById(id) + .orElseThrow(() -> new NotFoundException(ErrorCode.PROBLEM_STATISTIC_NOT_FOUND)); + } +} \ No newline at end of file diff --git a/src/main/java/com/moplus/moplus_server/statistic/Problem/service/CountStatisticsGetService.java b/src/main/java/com/moplus/moplus_server/statistic/Problem/service/CountStatisticsGetService.java new file mode 100644 index 00000000..983a87d6 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/statistic/Problem/service/CountStatisticsGetService.java @@ -0,0 +1,18 @@ +package com.moplus.moplus_server.statistic.Problem.service; + +import com.moplus.moplus_server.statistic.Problem.repository.ProblemSetStatisticRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CountStatisticsGetService { + + private final ProblemSetStatisticRepository problemSetStatisticRepository; + + @Transactional(readOnly = true) + public Long getProblemSetCount(Long id) { + return problemSetStatisticRepository.findByIdElseThrow(id).getSubmitCount(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/statistic/Problem/service/CountStatisticsUpdateService.java b/src/main/java/com/moplus/moplus_server/statistic/Problem/service/CountStatisticsUpdateService.java new file mode 100644 index 00000000..29801647 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/statistic/Problem/service/CountStatisticsUpdateService.java @@ -0,0 +1,33 @@ +package com.moplus.moplus_server.statistic.Problem.service; + +import com.moplus.moplus_server.statistic.Problem.domain.StatisticCounter; +import com.moplus.moplus_server.statistic.Problem.domain.StatisticEntityTarget; +import com.moplus.moplus_server.statistic.Problem.domain.StatisticFieldType; +import com.moplus.moplus_server.statistic.Problem.repository.ChildProblemStatisticRepository; +import com.moplus.moplus_server.statistic.Problem.repository.ProblemSetStatisticRepository; +import com.moplus.moplus_server.statistic.Problem.repository.ProblemStatisticRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CountStatisticsUpdateService { + private final ProblemStatisticRepository problemStatisticRepository; + private final ProblemSetStatisticRepository problemSetStatisticRepository; + private final ChildProblemStatisticRepository childProblemStatisticRepository; + + @Transactional + public void updateStatistics(Long id, StatisticFieldType type, StatisticEntityTarget target) { + StatisticCounter statistic = findStatistic(id, target); + statistic.updateCount(type); + } + + private StatisticCounter findStatistic(Long id, StatisticEntityTarget target) { + return switch (target) { + case PROBLEM -> problemStatisticRepository.findByIdElseThrow(id); + case PROBLEM_SET -> problemSetStatisticRepository.findByIdElseThrow(id); + case CHILD_PROBLEM -> childProblemStatisticRepository.findByIdElseThrow(id); + }; + } +} diff --git a/src/main/resources/application-datasource.yml b/src/main/resources/application-datasource.yml index 1876f672..f90f75cd 100644 --- a/src/main/resources/application-datasource.yml +++ b/src/main/resources/application-datasource.yml @@ -2,18 +2,13 @@ spring: config: activate: on-profile: "datasource" - datasource: - url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}?useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&tinyInt1isBit=false - driver-class-name: com.mysql.cj.jdbc.Driver - password: ${MYSQL_PASSWORD} - username: ${MYSQL_USERNAME} - hikari: - maximum-pool-size: 10 # default - connection-timeout: 3000 # 30 seconds in milliseconds - keepalive-time: 600000 # 5 minutes in milliseconds + jpa: properties: hibernate: dialect: org.hibernate.dialect.MySQLDialect - default_batch_fetch_size: 1000 + default_batch_fetch_size: 100 + + + diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index f1132665..e0b372ce 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -8,9 +8,18 @@ spring: show_sql: true format_sql: true use_sql_comments: true + highlight_sql: true hibernate: ddl-auto: update - + datasource: + url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}?useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&tinyInt1isBit=false + driver-class-name: com.mysql.cj.jdbc.Driver + password: ${MYSQL_PASSWORD} + username: ${MYSQL_USERNAME} + hikari: + maximum-pool-size: 10 # default + connection-timeout: 3000 # 30 seconds in milliseconds + keepalive-time: 600000 # 5 minutes in milliseconds swagger: servers: - url: http://localhost:8080 diff --git a/src/main/resources/application-security.yml b/src/main/resources/application-security.yml index c7ce0914..d94c6917 100644 --- a/src/main/resources/application-security.yml +++ b/src/main/resources/application-security.yml @@ -3,4 +3,11 @@ jwt: refresh-token-secret: ${JWT_REFRESH_TOKEN_SECRET:} access-token-expiration-time: ${JWT_ACCESS_TOKEN_EXPIRATION_TIME:7200} #2시간 refresh-token-expiration-time: ${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800} #7일 - issuer: ${JWT_ISSUER:} \ No newline at end of file + issuer: ${JWT_ISSUER:} + + +kakao: + login: + client_id: ${KAKAO_LOGIN_CLIENT_ID} + client_secret: ${KAKAO_LOGIN_CLIENT_SECRET} + redirect_uri: ${KAKAO_LOGIN_REDIRECT_URI} \ No newline at end of file diff --git a/src/main/resources/application-server.yml b/src/main/resources/application-server.yml new file mode 100644 index 00000000..268b5f53 --- /dev/null +++ b/src/main/resources/application-server.yml @@ -0,0 +1,8 @@ +server: + tomcat: + accept-count: 100 + max-connections: 8192 + + threads: + max: 200 + min-spare: 10 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 913c2a8c..8db87749 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,11 +3,12 @@ spring: active: ${profile} group: local: "local, datasource" - dev: "dev" - prod: "prod" + dev: "dev, datasource" + prod: "prod, datasource" include: - aws - security + - server mvc: ignore-default-favicon: true diff --git a/src/test/java/com/moplus/moplus_server/statistic/Problem/service/CountStatisticsUpdateServiceTest.java b/src/test/java/com/moplus/moplus_server/statistic/Problem/service/CountStatisticsUpdateServiceTest.java new file mode 100644 index 00000000..94e133e3 --- /dev/null +++ b/src/test/java/com/moplus/moplus_server/statistic/Problem/service/CountStatisticsUpdateServiceTest.java @@ -0,0 +1,87 @@ +package com.moplus.moplus_server.statistic.Problem.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +import com.moplus.moplus_server.statistic.Problem.domain.ChildProblemStatistic; +import com.moplus.moplus_server.statistic.Problem.domain.ProblemSetStatistic; +import com.moplus.moplus_server.statistic.Problem.domain.ProblemStatistic; +import com.moplus.moplus_server.statistic.Problem.domain.StatisticEntityTarget; +import com.moplus.moplus_server.statistic.Problem.domain.StatisticFieldType; +import com.moplus.moplus_server.statistic.Problem.repository.ChildProblemStatisticRepository; +import com.moplus.moplus_server.statistic.Problem.repository.ProblemSetStatisticRepository; +import com.moplus.moplus_server.statistic.Problem.repository.ProblemStatisticRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CountStatisticsUpdateServiceTest { + + @InjectMocks + private CountStatisticsUpdateService countStatisticsUpdateService; + + @Mock + private ProblemStatisticRepository problemStatisticRepository; + @Mock + private ProblemSetStatisticRepository problemSetStatisticRepository; + @Mock + private ChildProblemStatisticRepository childProblemStatisticRepository; + + @Test + void 문항_조회수_증가() { + // given + Long problemId = 1L; + ProblemStatistic problemStatistic = new ProblemStatistic(problemId); + given(problemStatisticRepository.findByIdElseThrow(problemId)) + .willReturn(problemStatistic); + + // when + countStatisticsUpdateService.updateStatistics(problemId, StatisticFieldType.VIEW, + StatisticEntityTarget.PROBLEM); + + // then + verify(problemStatisticRepository).findByIdElseThrow(problemId); + assertThat(problemStatistic.getViewCount()).isEqualTo(1L); + assertThat(problemStatistic.getSubmitCount()).isEqualTo(0L); + } + + @Test + void 문항세트_풀이수_증가() { + // given + Long problemSetId = 1L; + ProblemSetStatistic problemSetStatistic = new ProblemSetStatistic(problemSetId); + given(problemSetStatisticRepository.findByIdElseThrow(problemSetId)) + .willReturn(problemSetStatistic); + + // when + countStatisticsUpdateService.updateStatistics(problemSetId, StatisticFieldType.SUBMIT, + StatisticEntityTarget.PROBLEM_SET); + + // then + verify(problemSetStatisticRepository).findByIdElseThrow(problemSetId); + assertThat(problemSetStatistic.getSubmitCount()).isEqualTo(1L); + assertThat(problemSetStatistic.getViewCount()).isEqualTo(0L); + } + + @Test + void 새끼문항_조회수_증가() { + // given + Long childProblemId = 1L; + ChildProblemStatistic childProblemStatistic = new ChildProblemStatistic(childProblemId); + given(childProblemStatisticRepository.findByIdElseThrow(childProblemId)) + .willReturn(childProblemStatistic); + + // when + countStatisticsUpdateService.updateStatistics(childProblemId, StatisticFieldType.VIEW, + StatisticEntityTarget.CHILD_PROBLEM); + + // then + verify(childProblemStatisticRepository).findByIdElseThrow(childProblemId); + assertThat(childProblemStatistic.getViewCount()).isEqualTo(1L); + assertThat(childProblemStatistic.getSubmitCount()).isEqualTo(0L); + } +} \ No newline at end of file diff --git a/src/test/resources/insert-problem2.sql b/src/test/resources/insert-problem2.sql index 28340d6d..550bcc76 100644 --- a/src/test/resources/insert-problem2.sql +++ b/src/test/resources/insert-problem2.sql @@ -48,11 +48,12 @@ INSERT INTO child_problem (child_problem_id, image_url, answer_type, answer, - sequence) -VALUES (1, 1, 'child1.png', 'MULTIPLE_CHOICE', '1', 0), - (2, 1, 'child2.png', 'SHORT_ANSWER', '정답2', 1), - (3, 2, 'child3.png', 'MULTIPLE_CHOICE', '2', 0), - (4, 3, 'child4.png', 'SHORT_ANSWER', '3', 0); + sequence, + prescription_image_urls) +VALUES (1, 1, 'child1.png', 'MULTIPLE_CHOICE', '1', 0, 'child1_prescription1.png, child1_prescription2.png'), + (2, 1, 'child2.png', 'SHORT_ANSWER', '정답2', 1, 'child2_prescription1.png, child2_prescription2.png'), + (3, 2, 'child3.png', 'MULTIPLE_CHOICE', '2', 0, 'child3_prescription1.png, child3_prescription2.png'), + (4, 3, 'child4.png', 'SHORT_ANSWER', '3', 0, 'child4_prescription1.png, child4_prescription2.png'); -- 문제-컨셉 태그 연결 INSERT INTO problem_concept (problem_id, concept_tag_id) @@ -74,7 +75,7 @@ VALUES (1, 1), (4, 1), (4, 4); --- 유효하지 않은 문제 데이터 삽입 +-- 유효하지 않은 문제 데이터를 유효한 데이터로 수정 INSERT INTO problem (problem_id, problem_custom_id, practice_test_id, @@ -94,7 +95,25 @@ INSERT INTO problem (problem_id, is_confirmed, recommended_minute, recommended_second) -VALUES (4, '24052001004', 1, 4, 'GICHUL_PROBLEM', '', '', 1, '유효하지 않은 문제 설명', - '', 'mainAnalysis4.png', '', 'readingTip4.png', 'seniorTip4.png', - '', 'MULTIPLE_CHOICE', false, - null, null); \ No newline at end of file +VALUES (4, '24052001004', 1, 4, 'GICHUL_PROBLEM', '제목4', '4', 1, '유효한 문제로 수정', + 'mainProblem4.png', 'mainAnalysis4.png', 'mainHandwriting4.png', 'readingTip4.png', 'seniorTip4.png', + 'prescription7.png, prescription8.png', 'MULTIPLE_CHOICE', false, + 20, 0); + +-- problem 4에 대한 자식 문제 추가 +INSERT INTO child_problem (child_problem_id, + problem_id, + image_url, + answer_type, + answer, + sequence) +VALUES (5, 4, 'child5.png', 'MULTIPLE_CHOICE', '4', 0); + +-- problem 4와 자식 문제에 대한 컨셉 태그 추가 +INSERT INTO problem_concept (problem_id, concept_tag_id) +VALUES (4, 1), + (4, 3); + +INSERT INTO child_problem_concept (child_problem_id, concept_tag_id) +VALUES (5, 2), + (5, 3); \ No newline at end of file