diff --git a/.github/workflows/dev_deploy.yml b/.github/workflows/dev_deploy.yml index 9ea5484..e86d9ca 100644 --- a/.github/workflows/dev_deploy.yml +++ b/.github/workflows/dev_deploy.yml @@ -1,19 +1,17 @@ name: Breifing Dev CI/CD on: - push: - branches: - - develop pull_request: branches: - develop - types: [closed] + types: + - closed workflow_dispatch: # (2).수동 실행도 가능하도록 jobs: build: runs-on: ubuntu-latest # (3).OS환경 - if: (github.event_name == 'push' && github.ref == 'refs/heads/develop') || (github.event.pull_request && github.event.pull_request.merged == true) + if: github.event.pull_request.merged == true steps: - name: Checkout diff --git a/.github/workflows/release_deploy.yml b/.github/workflows/release_deploy.yml index 25f52d1..6ce6b3f 100644 --- a/.github/workflows/release_deploy.yml +++ b/.github/workflows/release_deploy.yml @@ -1,20 +1,17 @@ name: Breifing Release CI/CD on: - push: - branches: - - release pull_request: branches: - release - types: [closed] + types: + - closed workflow_dispatch: # (2).수동 실행도 가능하도록 jobs: build: runs-on: ubuntu-latest # (3).OS환경 - if: (github.event_name == 'push' && github.ref == 'refs/heads/release') || (github.event.pull_request && github.event.pull_request.merged == true) - + if: github.event.pull_request.merged == true steps: - name: Checkout diff --git a/README.md b/README.md index 1c93a13..c99a517 100644 --- a/README.md +++ b/README.md @@ -1 +1,110 @@ -# Briefing-Backend \ No newline at end of file +[![Briefing](https://github.com/Team-Shaka/Briefing-Backend/assets/53550707/d6a382f7-fee6-4a70-8ec7-5e9ce9803986)](https://linktr.ee/briefingnews) + + +# Briefing + +> 당신의 AI 뉴스 리더, Briefing • 백엔드 리포지토리 + +
+ +## 📰 서비스 소개 + + + + +![image](https://github.com/Team-Shaka/Briefing-Backend/assets/53550707/3fc54a19-3944-4168-a18f-701536ebe128) + +
+ +## 👨‍👩‍👧‍👦 팀 소개 +### Project Manager +| **진윤겸** | +| :------: | +| [
@Younkyum](https://github.com/Younkyum) | + +### Web Developer +| **서다원** | **조주희** | +| :------: | :------: | +| [
@Dawon00](https://github.com/Dawon00) | [
@juhui88](https://github.com/juhui88) | + +### Android Developer +| **김경록** | **김민서** | +| :------: | :------: | +| [
@gomsang](https://github.com/gomsang) | [
@kimwest00](https://github.com/kimwest00) | + +### IOS Developer +| **이보민** | **이전희** | +| :------: | :------: | +| [
@bome24](https://github.com/bome24) | [
@Jeonhui](https://github.com/Jeonhui) | + +### Server Developer +| **권현재** | **정성훈** | **최용욱** | +| :------: | :------: | :------: | +| [
@hyeonjerry](https://github.com/hyeonjerry) | [
@swa07016](https://github.com/swa07016) | [
@CYY1007](https://github.com/CYY1007) | + +
+ +## 👨‍💻 기술 스택 +
+ + + + + + + + +
+
+ + + +
+
+ +Spring Cloud +- write + +QueryDSL +- 컴파일 시점 문법 검사와 개발 편의성을 위해 QueryDSL을 사용했습니다. + +Redis +- Refresh Token 관리를 위해 Redis를 사용했습니다. + +AWS +- write + +
+ +## 📚 개발 과정 + +
+ +## 📁 프로젝트 아키텍쳐 + + + + + + + + + + + + + + + + + + + + + + + + + +
API 운영 ServerAPI 개발 Server
CrawlerCloud
API Server CI/CDCloud CI/CD
+ diff --git a/build.gradle b/build.gradle index 49d7e01..58cdea0 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.1.2' id 'io.spring.dependency-management' version '1.1.2' +// id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10' } group = 'briefing.info' @@ -23,6 +24,17 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + // eureka client 설정 + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + + // querydsl + // == 스프링 부트 3.0 이상 == + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE' @@ -46,6 +58,16 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' } +def querydslSrcDir = 'src/main/generated' + +clean { + delete file(querydslSrcDir) +} + +tasks.withType(JavaCompile) { + options.generatedSourceOutputDirectory = file(querydslSrcDir) +} + tasks.named('test') { useJUnitPlatform() } diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/generated/briefing/base/QBaseDateTimeEntity.java b/src/main/generated/briefing/base/QBaseDateTimeEntity.java new file mode 100644 index 0000000..55211cf --- /dev/null +++ b/src/main/generated/briefing/base/QBaseDateTimeEntity.java @@ -0,0 +1,39 @@ +package briefing.base; + +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; + + +/** + * QBaseDateTimeEntity is a Querydsl query type for BaseDateTimeEntity + */ +@Generated("com.querydsl.codegen.DefaultSupertypeSerializer") +public class QBaseDateTimeEntity extends EntityPathBase { + + private static final long serialVersionUID = 1261212710L; + + public static final QBaseDateTimeEntity baseDateTimeEntity = new QBaseDateTimeEntity("baseDateTimeEntity"); + + public final DateTimePath createdAt = createDateTime("createdAt", java.time.LocalDateTime.class); + + public final DateTimePath updatedAt = createDateTime("updatedAt", java.time.LocalDateTime.class); + + public QBaseDateTimeEntity(String variable) { + super(BaseDateTimeEntity.class, forVariable(variable)); + } + + public QBaseDateTimeEntity(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QBaseDateTimeEntity(PathMetadata metadata) { + super(BaseDateTimeEntity.class, metadata); + } + +} + diff --git a/src/main/generated/briefing/briefing/domain/QArticle.java b/src/main/generated/briefing/briefing/domain/QArticle.java new file mode 100644 index 0000000..db837a2 --- /dev/null +++ b/src/main/generated/briefing/briefing/domain/QArticle.java @@ -0,0 +1,51 @@ +package briefing.briefing.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; + + +/** + * QArticle is a Querydsl query type for Article + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QArticle extends EntityPathBase
{ + + private static final long serialVersionUID = -598074260L; + + public static final QArticle article = new QArticle("article"); + + public final briefing.base.QBaseDateTimeEntity _super = new briefing.base.QBaseDateTimeEntity(this); + + //inherited + public final DateTimePath createdAt = _super.createdAt; + + public final NumberPath id = createNumber("id", Long.class); + + public final StringPath press = createString("press"); + + public final StringPath title = createString("title"); + + //inherited + public final DateTimePath updatedAt = _super.updatedAt; + + public final StringPath url = createString("url"); + + public QArticle(String variable) { + super(Article.class, forVariable(variable)); + } + + public QArticle(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QArticle(PathMetadata metadata) { + super(Article.class, metadata); + } + +} + diff --git a/src/main/generated/briefing/briefing/domain/QBriefing.java b/src/main/generated/briefing/briefing/domain/QBriefing.java new file mode 100644 index 0000000..3a93bce --- /dev/null +++ b/src/main/generated/briefing/briefing/domain/QBriefing.java @@ -0,0 +1,62 @@ +package briefing.briefing.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; + + +/** + * QBriefing is a Querydsl query type for Briefing + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QBriefing extends EntityPathBase { + + private static final long serialVersionUID = 63849586L; + + public static final QBriefing briefing = new QBriefing("briefing"); + + public final briefing.base.QBaseDateTimeEntity _super = new briefing.base.QBaseDateTimeEntity(this); + + public final ListPath briefingArticles = this.createList("briefingArticles", BriefingArticle.class, QBriefingArticle.class, PathInits.DIRECT2); + + public final StringPath content = createString("content"); + + //inherited + public final DateTimePath createdAt = _super.createdAt; + + public final EnumPath gptModel = createEnum("gptModel", briefing.chatting.domain.GptModel.class); + + public final NumberPath id = createNumber("id", Long.class); + + public final NumberPath ranks = createNumber("ranks", Integer.class); + + public final StringPath subtitle = createString("subtitle"); + + public final EnumPath timeOfDay = createEnum("timeOfDay", TimeOfDay.class); + + public final StringPath title = createString("title"); + + public final EnumPath type = createEnum("type", BriefingType.class); + + //inherited + public final DateTimePath updatedAt = _super.updatedAt; + + public QBriefing(String variable) { + super(Briefing.class, forVariable(variable)); + } + + public QBriefing(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QBriefing(PathMetadata metadata) { + super(Briefing.class, metadata); + } + +} + diff --git a/src/main/generated/briefing/briefing/domain/QBriefingArticle.java b/src/main/generated/briefing/briefing/domain/QBriefingArticle.java new file mode 100644 index 0000000..fb3152a --- /dev/null +++ b/src/main/generated/briefing/briefing/domain/QBriefingArticle.java @@ -0,0 +1,62 @@ +package briefing.briefing.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; + + +/** + * QBriefingArticle is a Querydsl query type for BriefingArticle + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QBriefingArticle extends EntityPathBase { + + private static final long serialVersionUID = 37002276L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QBriefingArticle briefingArticle = new QBriefingArticle("briefingArticle"); + + public final briefing.base.QBaseDateTimeEntity _super = new briefing.base.QBaseDateTimeEntity(this); + + public final QArticle article; + + public final QBriefing briefing; + + //inherited + public final DateTimePath createdAt = _super.createdAt; + + public final NumberPath id = createNumber("id", Long.class); + + //inherited + public final DateTimePath updatedAt = _super.updatedAt; + + public QBriefingArticle(String variable) { + this(BriefingArticle.class, forVariable(variable), INITS); + } + + public QBriefingArticle(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QBriefingArticle(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QBriefingArticle(PathMetadata metadata, PathInits inits) { + this(BriefingArticle.class, metadata, inits); + } + + public QBriefingArticle(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.article = inits.isInitialized("article") ? new QArticle(forProperty("article")) : null; + this.briefing = inits.isInitialized("briefing") ? new QBriefing(forProperty("briefing")) : null; + } + +} + diff --git a/src/main/generated/briefing/chatting/domain/QChatting.java b/src/main/generated/briefing/chatting/domain/QChatting.java new file mode 100644 index 0000000..0a9ccae --- /dev/null +++ b/src/main/generated/briefing/chatting/domain/QChatting.java @@ -0,0 +1,50 @@ +package briefing.chatting.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; + + +/** + * QChatting is a Querydsl query type for Chatting + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QChatting extends EntityPathBase { + + private static final long serialVersionUID = -1751649618L; + + public static final QChatting chatting = new QChatting("chatting"); + + public final briefing.base.QBaseDateTimeEntity _super = new briefing.base.QBaseDateTimeEntity(this); + + //inherited + public final DateTimePath createdAt = _super.createdAt; + + public final NumberPath id = createNumber("id", Long.class); + + public final ListPath messages = this.createList("messages", Message.class, QMessage.class, PathInits.DIRECT2); + + public final StringPath title = createString("title"); + + //inherited + public final DateTimePath updatedAt = _super.updatedAt; + + public QChatting(String variable) { + super(Chatting.class, forVariable(variable)); + } + + public QChatting(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QChatting(PathMetadata metadata) { + super(Chatting.class, metadata); + } + +} + diff --git a/src/main/generated/briefing/chatting/domain/QMessage.java b/src/main/generated/briefing/chatting/domain/QMessage.java new file mode 100644 index 0000000..d9196c9 --- /dev/null +++ b/src/main/generated/briefing/chatting/domain/QMessage.java @@ -0,0 +1,57 @@ +package briefing.chatting.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; + + +/** + * QMessage is a Querydsl query type for Message + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QMessage extends EntityPathBase { + + private static final long serialVersionUID = -1226188129L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QMessage message = new QMessage("message"); + + public final QChatting chatting; + + public final StringPath content = createString("content"); + + public final DateTimePath createdAt = createDateTime("createdAt", java.time.LocalDateTime.class); + + public final NumberPath id = createNumber("id", Long.class); + + public final EnumPath role = createEnum("role", MessageRole.class); + + public QMessage(String variable) { + this(Message.class, forVariable(variable), INITS); + } + + public QMessage(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QMessage(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QMessage(PathMetadata metadata, PathInits inits) { + this(Message.class, metadata, inits); + } + + public QMessage(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.chatting = inits.isInitialized("chatting") ? new QChatting(forProperty("chatting")) : null; + } + +} + diff --git a/src/main/generated/briefing/member/domain/QMember.java b/src/main/generated/briefing/member/domain/QMember.java new file mode 100644 index 0000000..1f80880 --- /dev/null +++ b/src/main/generated/briefing/member/domain/QMember.java @@ -0,0 +1,60 @@ +package briefing.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; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QMember is a Querydsl query type for Member + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QMember extends EntityPathBase { + + private static final long serialVersionUID = 2051544022L; + + public static final QMember member = new QMember("member1"); + + public final briefing.base.QBaseDateTimeEntity _super = new briefing.base.QBaseDateTimeEntity(this); + + //inherited + public final DateTimePath createdAt = _super.createdAt; + + public final NumberPath id = createNumber("id", Long.class); + + public final StringPath nickName = createString("nickName"); + + public final StringPath profileImgUrl = createString("profileImgUrl"); + + public final EnumPath role = createEnum("role", MemberRole.class); + + public final ListPath scrapList = this.createList("scrapList", briefing.scrap.domain.Scrap.class, briefing.scrap.domain.QScrap.class, PathInits.DIRECT2); + + public final StringPath socialId = createString("socialId"); + + public final EnumPath socialType = createEnum("socialType", SocialType.class); + + public final EnumPath status = createEnum("status", MemberStatus.class); + + //inherited + public final DateTimePath updatedAt = _super.updatedAt; + + public QMember(String variable) { + super(Member.class, forVariable(variable)); + } + + public QMember(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QMember(PathMetadata metadata) { + super(Member.class, metadata); + } + +} + diff --git a/src/main/generated/briefing/scrap/domain/QScrap.java b/src/main/generated/briefing/scrap/domain/QScrap.java new file mode 100644 index 0000000..13e31f8 --- /dev/null +++ b/src/main/generated/briefing/scrap/domain/QScrap.java @@ -0,0 +1,62 @@ +package briefing.scrap.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; + + +/** + * QScrap is a Querydsl query type for Scrap + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QScrap extends EntityPathBase { + + private static final long serialVersionUID = -881090678L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QScrap scrap = new QScrap("scrap"); + + public final briefing.base.QBaseDateTimeEntity _super = new briefing.base.QBaseDateTimeEntity(this); + + public final briefing.briefing.domain.QBriefing briefing; + + //inherited + public final DateTimePath createdAt = _super.createdAt; + + public final NumberPath id = createNumber("id", Long.class); + + public final briefing.member.domain.QMember member; + + //inherited + public final DateTimePath updatedAt = _super.updatedAt; + + public QScrap(String variable) { + this(Scrap.class, forVariable(variable), INITS); + } + + public QScrap(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QScrap(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QScrap(PathMetadata metadata, PathInits inits) { + this(Scrap.class, metadata, inits); + } + + public QScrap(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.briefing = inits.isInitialized("briefing") ? new briefing.briefing.domain.QBriefing(forProperty("briefing")) : null; + this.member = inits.isInitialized("member") ? new briefing.member.domain.QMember(forProperty("member")) : null; + } + +} + diff --git a/src/main/java/briefing/briefing/api/BriefingApi.java b/src/main/java/briefing/briefing/api/BriefingApi.java index 43c7c8e..5847219 100644 --- a/src/main/java/briefing/briefing/api/BriefingApi.java +++ b/src/main/java/briefing/briefing/api/BriefingApi.java @@ -3,31 +3,31 @@ import briefing.briefing.application.BriefingCommandService; import briefing.briefing.application.BriefingQueryService; import briefing.briefing.application.dto.*; +import briefing.briefing.domain.Briefing; import briefing.briefing.domain.BriefingType; import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; import java.util.Optional; +import briefing.common.enums.APIVersion; import briefing.common.response.CommonResponse; import briefing.member.domain.Member; import briefing.scrap.application.ScrapQueryService; import briefing.security.handler.annotation.AuthMember; + +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; + import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @Tag(name = "03-Briefing \uD83D\uDCF0",description = "브리핑 관련 API") @RestController -@RequestMapping("/briefings") @RequiredArgsConstructor public class BriefingApi { @@ -35,34 +35,77 @@ public class BriefingApi { private final BriefingCommandService briefingCommandService; private final ScrapQueryService scrapQueryService; - @GetMapping + @GetMapping("/v2/briefings") + @Operation(summary = "03-01Briefing \uD83D\uDCF0 브리핑 목록 조회 V2", description = "") + public CommonResponse findBriefingsV2( + @ParameterObject @ModelAttribute BriefingRequestParam.BriefingPreviewListParam params + ) { + List briefingList = briefingQueryService.findBriefings(params, APIVersion.V2); + return CommonResponse.onSuccess(BriefingConverter.toBriefingPreviewListDTOV2(params.getDate(), briefingList)); + } + + @GetMapping("/briefings") + @Parameter(name = "timeOfDay", hidden = true) + @Operation(summary = "03-01Briefing \uD83D\uDCF0 브리핑 목록 조회 V1", description = "") public CommonResponse findBriefings( - @RequestParam("type") final BriefingType type, - @RequestParam("date") final LocalDate date + @ParameterObject @ModelAttribute BriefingRequestParam.BriefingPreviewListParam params ) { - return CommonResponse.onSuccess(BriefingConverter.toBriefingPreviewListDTO(date, briefingQueryService.findBriefings(type, date))); + + List briefingList = briefingQueryService.findBriefings(params, APIVersion.V1); + return CommonResponse.onSuccess(BriefingConverter.toBriefingPreviewListDTO(params.getDate(), briefingList)); } - @GetMapping("/{id}") + @Deprecated + @Operation(summary = "키워드 전달 V2 임시 API", description = "키워드 전달 V2 임시 API 입니다. 응답은 무조건 동일합니다. type만 주신걸 담아서 드립니다.") + @ApiResponse(responseCode = "1000", description = "OK, 성공") + @GetMapping("/briefings/temp") + public CommonResponse findBriefingsV2Temp( + @RequestParam("type") final BriefingType type, + @RequestParam("date") final LocalDate date + ){ + List idList = Arrays.asList(346L, 347L, 348L, 349L, 350L); + return CommonResponse.onSuccess(BriefingConverter.toBriefingPreviewV2TempListDTO(date,idList,type)); + } + + @GetMapping("/v2/briefings/{id}") + @Operation(summary = "03-02Briefing \uD83D\uDCF0 브리핑 단건 조회 V2", description = "") @Parameter(name = "member", hidden = true) - public CommonResponse findBriefing(@PathVariable final Long id, @AuthMember Member member) { + public CommonResponse findBriefingV2( + @PathVariable final Long id, + @AuthMember Member member + ) { + + Boolean isScrap = Optional.ofNullable(member) + .map(m -> scrapQueryService.existsByMemberIdAndBriefingId(m.getId(), id)) + .orElseGet(() -> Boolean.FALSE); + + Boolean isBriefingOpen = false; + Boolean isWarning = false; + + return CommonResponse.onSuccess(BriefingConverter.toBriefingDetailDTOV2(briefingQueryService.findBriefing(id, APIVersion.V2), isScrap, isBriefingOpen, isWarning)); + } + + @GetMapping("/briefings/{id}") + @Parameter(name = "member", hidden = true) + @Operation(summary = "03-02Briefing \uD83D\uDCF0 브리핑 단건 조회 V1", description = "") + public CommonResponse findBriefing( + @PathVariable final Long id, + @AuthMember Member member + ) { Boolean isScrap = Optional.ofNullable(member) .map(m -> scrapQueryService.existsByMemberIdAndBriefingId(m.getId(), id)) .orElseGet(() -> Boolean.FALSE); - /* - TODO - 업데이트가 확정되면 로직을 거쳐 isBriefingOpen과 isWarning을 세팅해주어야합니다. - */ Boolean isBriefingOpen = false; Boolean isWarning = false; - return CommonResponse.onSuccess(BriefingConverter.toBriefingDetailDTO(briefingQueryService.findBriefing(id), isScrap, isBriefingOpen, isWarning)); + return CommonResponse.onSuccess(BriefingConverter.toBriefingDetailDTO(briefingQueryService.findBriefing(id, APIVersion.V1), isScrap, isBriefingOpen, isWarning)); } - @PostMapping + @PostMapping("/briefings") @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "03-03Briefing \uD83D\uDCF0 브리핑 등록", description = "") public void createBriefing(@RequestBody final BriefingRequestDTO.BriefingCreate request) { briefingCommandService.createBriefing(request); } diff --git a/src/main/java/briefing/briefing/api/BriefingConverter.java b/src/main/java/briefing/briefing/api/BriefingConverter.java index 56160f3..f192e74 100644 --- a/src/main/java/briefing/briefing/api/BriefingConverter.java +++ b/src/main/java/briefing/briefing/api/BriefingConverter.java @@ -7,12 +7,23 @@ import briefing.briefing.domain.BriefingType; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; public class BriefingConverter { + public static BriefingResponseDTO.BriefingPreviewDTOV2 toBriefingPreviewDTOV2(Briefing briefing){ + return BriefingResponseDTO.BriefingPreviewDTOV2.builder() + .id(briefing.getId()) + .ranks(briefing.getRanks()) + .title(briefing.getTitle()) + .subtitle(briefing.getSubtitle()) + .scrapCount(briefing.getScrapCount()) + .build(); + } + public static BriefingResponseDTO.BriefingPreviewDTO toBriefingPreviewDTO(Briefing briefing){ return BriefingResponseDTO.BriefingPreviewDTO.builder() .id(briefing.getId()) @@ -22,12 +33,32 @@ public static BriefingResponseDTO.BriefingPreviewDTO toBriefingPreviewDTO(Briefi .build(); } + private static LocalDateTime getPreviewListDTOCreatedAt(final LocalDate date, List briefingList) { + if(!briefingList.isEmpty()) { + return briefingList.get(0).getCreatedAt(); + } + if(date != null) { + return date.atTime(3,0); + } + return LocalDateTime.now(); + } + + public static BriefingResponseDTO.BriefingPreviewListDTOV2 toBriefingPreviewListDTOV2(final LocalDate date, List briefingList){ + final List briefingPreviewDTOList = briefingList.stream() + .map(BriefingConverter::toBriefingPreviewDTOV2).toList(); + + return BriefingResponseDTO.BriefingPreviewListDTOV2.builder() + .createdAt(getPreviewListDTOCreatedAt(date, briefingList)) + .briefings(briefingPreviewDTOList) + .build(); + } + public static BriefingResponseDTO.BriefingPreviewListDTO toBriefingPreviewListDTO(final LocalDate date, List briefingList){ final List briefingPreviewDTOList = briefingList.stream() .map(BriefingConverter::toBriefingPreviewDTO).toList(); return BriefingResponseDTO.BriefingPreviewListDTO.builder() - .createdAt(date.atTime(3,0)) + .createdAt(getPreviewListDTOCreatedAt(date, briefingList)) .briefings(briefingPreviewDTOList) .build(); } @@ -65,13 +96,43 @@ public static BriefingResponseDTO.BriefingDetailDTO toBriefingDetailDTO( .build(); } + public static BriefingResponseDTO.BriefingDetailDTOV2 toBriefingDetailDTOV2( + Briefing briefing, + Boolean isScrap, + Boolean isBriefingOpen, + Boolean isWarning + ){ + + List articleResponseDTOList = briefing.getBriefingArticles().stream() + .map(article -> toArticleResponseDTO(article.getArticle())).toList(); + + return BriefingResponseDTO.BriefingDetailDTOV2.builder() + .id(briefing.getId()) + .ranks(briefing.getRanks()) + .title(briefing.getTitle()) + .subtitle(briefing.getSubtitle()) + .content(briefing.getContent()) + .date(briefing.getCreatedAt().toLocalDate()) + .articles(articleResponseDTOList) + .isScrap(isScrap) + .isBriefingOpen(isBriefingOpen) + .isWarning(isWarning) + .scrapCount(briefing.getScrapCount()) + .gptModel(briefing.getGptModel()) + .timeOfDay(briefing.getTimeOfDay()) + .type(briefing.getType()) + .build(); + } + public static Briefing toBriefing(BriefingRequestDTO.BriefingCreate request){ return Briefing.builder() - .type(BriefingType.KOREA) + .type(request.getBriefingType()) .ranks(request.getRanks()) .title(request.getTitle()) .subtitle(request.getSubtitle()) .content(request.getContent()) + .gptModel(request.getGptModel()) + .timeOfDay(request.getTimeOfDay()) .build(); } @@ -82,4 +143,61 @@ public static Article toArticle(BriefingRequestDTO.ArticleCreateDTO request){ .url(request.getUrl()) .build(); } + + + public static BriefingResponseDTO.BriefingPreviewV2TempDTO toBriefingPreviewV2TempDTO(Long id){ + Integer rank = null; + String title = null; + String subTitle = null; + Integer scrapCount = null; + + if (id.equals(346L)){ + rank = 1; + title = "소셜 1"; + subTitle = "브리핑 부제목 1"; + scrapCount = 1234; + } + else if (id.equals(347L)){ + rank = 2; + title = "소셜 2"; + subTitle = "브리핑 부제목 2"; + scrapCount = 123; + }else if(id.equals(348L)){ + rank = 3; + title = "소셜 3"; + subTitle = "브리핑 부제목 3"; + scrapCount = 13; + }else if (id.equals(349L)){ + rank = 4; + title = "소셜 4"; + subTitle = "브리핑 부제목 4"; + scrapCount = 12323; + }else if (id.equals(350L)){ + rank = 5; + title = "소셜 5"; + subTitle = "브리핑 부제목 5"; + scrapCount = 123; + } + + return BriefingResponseDTO.BriefingPreviewV2TempDTO.builder() + .id(id) + .ranks(rank) + .title(title) + .subtitle(subTitle) + .scrapCount(scrapCount) + .build(); + } + + public static BriefingResponseDTO.BriefingV2PreviewListDTO toBriefingPreviewV2TempListDTO(final LocalDate date, List temp, BriefingType briefingType){ + List tempDTOList = temp.stream() + .map( + BriefingConverter::toBriefingPreviewV2TempDTO + ).toList(); + + return BriefingResponseDTO.BriefingV2PreviewListDTO.builder() + .createdAt(date.atTime(3,0)) + .type(briefingType.getValue()) + .briefings(tempDTOList) + .build(); + } } diff --git a/src/main/java/briefing/briefing/application/BriefingQueryService.java b/src/main/java/briefing/briefing/application/BriefingQueryService.java index 849a0ab..1679355 100644 --- a/src/main/java/briefing/briefing/application/BriefingQueryService.java +++ b/src/main/java/briefing/briefing/application/BriefingQueryService.java @@ -1,13 +1,12 @@ package briefing.briefing.application; +import briefing.briefing.application.context.BriefingQueryContext; +import briefing.briefing.application.context.BriefingQueryContextFactory; +import briefing.briefing.application.dto.BriefingRequestParam; import briefing.briefing.domain.Briefing; -import briefing.briefing.domain.BriefingType; -import briefing.briefing.domain.repository.BriefingRepository; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; import java.util.List; +import briefing.common.enums.APIVersion; import briefing.exception.ErrorCode; import briefing.exception.handler.BriefingException; import lombok.RequiredArgsConstructor; @@ -19,22 +18,14 @@ @RequiredArgsConstructor public class BriefingQueryService { - private final BriefingRepository briefingRepository; - - public List findBriefings(final BriefingType type, final LocalDate date) { - final LocalDateTime startDateTime = date.atStartOfDay(); - final LocalDateTime endDateTime = date.atTime(LocalTime.MAX); - - final List briefings = briefingRepository.findAllByTypeAndCreatedAtBetweenOrderByRanks( - type, startDateTime, endDateTime); - - return briefings; + public List findBriefings(BriefingRequestParam.BriefingPreviewListParam params, APIVersion version) { + BriefingQueryContext briefingQueryContext = BriefingQueryContextFactory.getContextByVersion(version); + return briefingQueryContext.findBriefings(params); } - public Briefing findBriefing(final Long id) { - final Briefing briefing = briefingRepository.findById(id) + public Briefing findBriefing(final Long id, final APIVersion version) { + BriefingQueryContext briefingQueryContext = BriefingQueryContextFactory.getContextByVersion(version); + return briefingQueryContext.findById(id) .orElseThrow(() -> new BriefingException(ErrorCode.NOT_FOUND_BRIEFING)); - - return briefing; } } diff --git a/src/main/java/briefing/briefing/application/context/BriefingQueryContext.java b/src/main/java/briefing/briefing/application/context/BriefingQueryContext.java new file mode 100644 index 0000000..896be83 --- /dev/null +++ b/src/main/java/briefing/briefing/application/context/BriefingQueryContext.java @@ -0,0 +1,22 @@ +package briefing.briefing.application.context; + +import briefing.briefing.application.dto.BriefingRequestParam; +import briefing.briefing.application.strategy.BriefingQueryStrategy; +import briefing.briefing.domain.Briefing; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +public class BriefingQueryContext { + private final BriefingQueryStrategy briefingQueryStrategy; + + public List findBriefings(BriefingRequestParam.BriefingPreviewListParam params) { + return this.briefingQueryStrategy.findBriefings(params); + } + + public Optional findById(Long id) { + return this.briefingQueryStrategy.findById(id); + } +} diff --git a/src/main/java/briefing/briefing/application/context/BriefingQueryContextFactory.java b/src/main/java/briefing/briefing/application/context/BriefingQueryContextFactory.java new file mode 100644 index 0000000..7e8fdaf --- /dev/null +++ b/src/main/java/briefing/briefing/application/context/BriefingQueryContextFactory.java @@ -0,0 +1,37 @@ +package briefing.briefing.application.context; + +import briefing.briefing.application.strategy.BriefingQueryStrategy; +import briefing.briefing.application.strategy.BriefingV1QueryStrategy; +import briefing.briefing.application.strategy.BriefingV2QueryStrategy; +import briefing.briefing.domain.repository.BriefingRepository; +import briefing.common.enums.APIVersion; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class BriefingQueryContextFactory { + + private final BriefingRepository briefingRepository; + private static BriefingQueryContext staticBriefingQueryContextV1; + private static BriefingQueryContext staticBriefingQueryContextV2; + + @PostConstruct + private void init() { + staticBriefingQueryContextV1 = createContext(new BriefingV1QueryStrategy(briefingRepository)); + staticBriefingQueryContextV2 = createContext(new BriefingV2QueryStrategy(briefingRepository)); + } + + private static BriefingQueryContext createContext(BriefingQueryStrategy strategy) { + return new BriefingQueryContext(strategy); + } + + public static BriefingQueryContext getContextByVersion(APIVersion version) { + return switch (version) { + case V1 -> staticBriefingQueryContextV1; + case V2 -> staticBriefingQueryContextV2; + }; + } +} + diff --git a/src/main/java/briefing/briefing/application/dto/BriefingRequestDTO.java b/src/main/java/briefing/briefing/application/dto/BriefingRequestDTO.java index b133194..a2389fa 100644 --- a/src/main/java/briefing/briefing/application/dto/BriefingRequestDTO.java +++ b/src/main/java/briefing/briefing/application/dto/BriefingRequestDTO.java @@ -1,5 +1,8 @@ package briefing.briefing.application.dto; +import briefing.briefing.domain.BriefingType; +import briefing.briefing.domain.TimeOfDay; +import briefing.chatting.domain.GptModel; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; @@ -21,5 +24,8 @@ public static class BriefingCreate{ String subtitle; @JsonProperty("context") String content; List articles; + GptModel gptModel = GptModel.GPT_4; + TimeOfDay timeOfDay = TimeOfDay.MORNING; + BriefingType briefingType = BriefingType.KOREA; } } diff --git a/src/main/java/briefing/briefing/application/dto/BriefingRequestParam.java b/src/main/java/briefing/briefing/application/dto/BriefingRequestParam.java new file mode 100644 index 0000000..196837f --- /dev/null +++ b/src/main/java/briefing/briefing/application/dto/BriefingRequestParam.java @@ -0,0 +1,27 @@ +package briefing.briefing.application.dto; + +import briefing.briefing.domain.BriefingType; +import briefing.briefing.domain.TimeOfDay; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +import java.time.LocalDate; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class BriefingRequestParam { + + @Builder + @Getter @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class BriefingPreviewListParam { + @NotNull + private BriefingType type; + private LocalDate date; + private TimeOfDay timeOfDay = TimeOfDay.MORNING; + + public boolean isPresentDate() { + return date != null; + } + } +} diff --git a/src/main/java/briefing/briefing/application/dto/BriefingResponseDTO.java b/src/main/java/briefing/briefing/application/dto/BriefingResponseDTO.java index 6136bac..41876dc 100644 --- a/src/main/java/briefing/briefing/application/dto/BriefingResponseDTO.java +++ b/src/main/java/briefing/briefing/application/dto/BriefingResponseDTO.java @@ -1,5 +1,8 @@ package briefing.briefing.application.dto; +import briefing.briefing.domain.BriefingType; +import briefing.briefing.domain.TimeOfDay; +import briefing.chatting.domain.GptModel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -22,6 +25,19 @@ public static class ArticleResponseDTO{ String url; } + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class BriefingPreviewDTOV2{ + Long id; + Integer ranks; + String title; + String subtitle; + @Builder.Default + Integer scrapCount = 0; + } + @Builder @Getter @NoArgsConstructor @@ -50,6 +66,40 @@ public static class BriefingDetailDTO{ Boolean isWarning; } + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class BriefingDetailDTOV2{ + Long id; + Integer ranks; + String title; + String subtitle; + String content; + LocalDate date; + List articles; + Boolean isScrap; + Boolean isBriefingOpen; + Boolean isWarning; + @Builder.Default + Integer scrapCount = 0; + @Builder.Default + GptModel gptModel = GptModel.GPT_3_5_TURBO; + @Builder.Default + TimeOfDay timeOfDay = TimeOfDay.MORNING; + @Builder.Default + BriefingType type = BriefingType.KOREA; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class BriefingPreviewListDTOV2{ + LocalDateTime createdAt; + List briefings; + } + @Builder @Getter @NoArgsConstructor @@ -58,4 +108,26 @@ public static class BriefingPreviewListDTO{ LocalDateTime createdAt; List briefings; } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class BriefingV2PreviewListDTO{ + LocalDateTime createdAt; + String type; + List briefings; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class BriefingPreviewV2TempDTO{ + Long id; + Integer ranks; + String title; + String subtitle; + Integer scrapCount; + } } diff --git a/src/main/java/briefing/briefing/application/strategy/BriefingQueryStrategy.java b/src/main/java/briefing/briefing/application/strategy/BriefingQueryStrategy.java new file mode 100644 index 0000000..96c313a --- /dev/null +++ b/src/main/java/briefing/briefing/application/strategy/BriefingQueryStrategy.java @@ -0,0 +1,13 @@ +package briefing.briefing.application.strategy; + +import briefing.briefing.application.dto.BriefingRequestParam; +import briefing.briefing.domain.Briefing; + +import java.util.List; +import java.util.Optional; + +public interface BriefingQueryStrategy { + List findBriefings(BriefingRequestParam.BriefingPreviewListParam params); + + Optional findById(Long id); +} diff --git a/src/main/java/briefing/briefing/application/strategy/BriefingV1QueryStrategy.java b/src/main/java/briefing/briefing/application/strategy/BriefingV1QueryStrategy.java new file mode 100644 index 0000000..283feac --- /dev/null +++ b/src/main/java/briefing/briefing/application/strategy/BriefingV1QueryStrategy.java @@ -0,0 +1,33 @@ +package briefing.briefing.application.strategy; + +import briefing.briefing.application.dto.BriefingRequestParam; +import briefing.briefing.domain.Briefing; +import briefing.briefing.domain.BriefingType; +import briefing.briefing.domain.repository.BriefingRepository; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +public class BriefingV1QueryStrategy implements BriefingQueryStrategy { + + private final BriefingRepository briefingRepository; + + @Override + public List findBriefings(BriefingRequestParam.BriefingPreviewListParam params) { + final LocalDateTime startDateTime = params.getDate().atStartOfDay(); + final LocalDateTime endDateTime = params.getDate().atTime(LocalTime.MAX); + + List briefingList = briefingRepository.findAllByTypeAndCreatedAtBetweenOrderByRanks(params.getType(), startDateTime, endDateTime); + if(briefingList.isEmpty()) return briefingRepository.findTop10ByTypeOrderByCreatedAtDesc(BriefingType.SOCIAL); + return briefingList; + } + + @Override + public Optional findById(Long id) { + return briefingRepository.findById(id); + } +} diff --git a/src/main/java/briefing/briefing/application/strategy/BriefingV2QueryStrategy.java b/src/main/java/briefing/briefing/application/strategy/BriefingV2QueryStrategy.java new file mode 100644 index 0000000..61bd421 --- /dev/null +++ b/src/main/java/briefing/briefing/application/strategy/BriefingV2QueryStrategy.java @@ -0,0 +1,38 @@ +package briefing.briefing.application.strategy; + +import briefing.briefing.application.dto.BriefingRequestParam; +import briefing.briefing.domain.Briefing; +import briefing.briefing.domain.repository.BriefingRepository; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +public class BriefingV2QueryStrategy implements BriefingQueryStrategy{ + + private final BriefingRepository briefingRepository; + + @Override + public List findBriefings(BriefingRequestParam.BriefingPreviewListParam params) { + if(params.isPresentDate()) { + final LocalDateTime startDateTime = params.getDate().atStartOfDay(); + final LocalDateTime endDateTime = params.getDate().atTime(LocalTime.MAX); + + return briefingRepository.findBriefingsWithScrapCount( + params.getType(), startDateTime, endDateTime, params.getTimeOfDay()); + } + + List briefingList = briefingRepository.findTop10ByTypeOrderByCreatedAtDesc(params.getType()); + Collections.reverse(briefingList); + return briefingList; + } + + @Override + public Optional findById(Long id) { + return briefingRepository.findByIdWithScrapCount(id); + } +} diff --git a/src/main/java/briefing/briefing/domain/Briefing.java b/src/main/java/briefing/briefing/domain/Briefing.java index 201eef2..e875d56 100644 --- a/src/main/java/briefing/briefing/domain/Briefing.java +++ b/src/main/java/briefing/briefing/domain/Briefing.java @@ -1,15 +1,9 @@ package briefing.briefing.domain; import briefing.base.BaseDateTimeEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; +import briefing.chatting.domain.GptModel; +import jakarta.persistence.*; + import java.util.ArrayList; import java.util.List; @@ -24,26 +18,40 @@ public class Briefing extends BaseDateTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Enumerated(EnumType.STRING) @Column(nullable = false) private BriefingType type; + @Column(nullable = false) private Integer ranks; + @Column(nullable = false) private String title; + @Column(nullable = false) private String subtitle; + @Column(nullable = false, length = 1000) private String content; + + @Builder.Default @OneToMany(mappedBy = "briefing", fetch = FetchType.LAZY) private List briefingArticles = new ArrayList<>(); -// public Briefing(final BriefingType type, final Integer ranks, final String title, -// final String subtitle, final String content) { -// this.type = type; -// this.ranks = ranks; -// this.title = title; -// this.subtitle = subtitle; -// this.content = content; -// } + @Builder.Default + @Transient + private Integer scrapCount = 0; + + @Builder.Default + @Enumerated(EnumType.STRING) + private TimeOfDay timeOfDay = TimeOfDay.MORNING; + + @Builder.Default + @Enumerated(EnumType.STRING) + private GptModel gptModel = GptModel.GPT_3_5_TURBO; + + public void setScrapCount(Integer scrapCount) { + this.scrapCount = scrapCount; + } } diff --git a/src/main/java/briefing/briefing/domain/BriefingType.java b/src/main/java/briefing/briefing/domain/BriefingType.java index 4660d00..99e18de 100644 --- a/src/main/java/briefing/briefing/domain/BriefingType.java +++ b/src/main/java/briefing/briefing/domain/BriefingType.java @@ -11,7 +11,10 @@ @RequiredArgsConstructor public enum BriefingType { KOREA("Korea"), - GLOBAL("Global"); + GLOBAL("Global"), + SOCIAL("Social"), + SCIENCE("Science"), + ECONOMY("Economy"); private final String value; diff --git a/src/main/java/briefing/briefing/domain/TimeOfDay.java b/src/main/java/briefing/briefing/domain/TimeOfDay.java new file mode 100644 index 0000000..abd3fbb --- /dev/null +++ b/src/main/java/briefing/briefing/domain/TimeOfDay.java @@ -0,0 +1,30 @@ +package briefing.briefing.domain; + +import briefing.exception.ErrorCode; +import briefing.exception.handler.BriefingException; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +@AllArgsConstructor +public enum TimeOfDay { + MORNING("Morning"), + EVENING("Evening"); + + private final String description; + + public static TimeOfDay findByValue(String type) { + return Arrays.stream(values()) + .filter(value -> value.getDescription().equals(type)) + .findAny() + .orElseThrow(() -> new BriefingException(ErrorCode.NOT_FOUND_TYPE)); + } + + @JsonValue + String getTimeOfDay() { + return this.getDescription(); + } +} diff --git a/src/main/java/briefing/briefing/domain/repository/BriefingCustomRepository.java b/src/main/java/briefing/briefing/domain/repository/BriefingCustomRepository.java new file mode 100644 index 0000000..3c34718 --- /dev/null +++ b/src/main/java/briefing/briefing/domain/repository/BriefingCustomRepository.java @@ -0,0 +1,17 @@ +package briefing.briefing.domain.repository; + +import briefing.briefing.domain.Briefing; +import briefing.briefing.domain.BriefingType; +import briefing.briefing.domain.TimeOfDay; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface BriefingCustomRepository { + List findBriefingsWithScrapCount(BriefingType type, LocalDateTime start, LocalDateTime end, TimeOfDay timeOfDay); + + List findTop10ByTypeOrderByCreatedAtDesc(BriefingType type); + + Optional findByIdWithScrapCount(Long id); +} diff --git a/src/main/java/briefing/briefing/domain/repository/BriefingCustomRepositoryImpl.java b/src/main/java/briefing/briefing/domain/repository/BriefingCustomRepositoryImpl.java new file mode 100644 index 0000000..d04bb07 --- /dev/null +++ b/src/main/java/briefing/briefing/domain/repository/BriefingCustomRepositoryImpl.java @@ -0,0 +1,101 @@ +package briefing.briefing.domain.repository; + +import briefing.briefing.domain.Briefing; +import briefing.briefing.domain.BriefingType; +import briefing.briefing.domain.QBriefing; +import briefing.briefing.domain.TimeOfDay; +import briefing.scrap.domain.QScrap; +import com.querydsl.core.Tuple; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.ArrayList; +import java.util.Optional; +import java.util.stream.Collectors; + + +@Repository +@RequiredArgsConstructor +public class BriefingCustomRepositoryImpl implements BriefingCustomRepository { + private final JPAQueryFactory queryFactory; + + @Override + public List findBriefingsWithScrapCount(BriefingType type, LocalDateTime start, LocalDateTime end, TimeOfDay timeOfDay) { + QBriefing briefing = QBriefing.briefing; + QScrap scrap = QScrap.scrap; + + // 쿼리 결과를 Tuple로 가져옵니다. + List results = queryFactory.select(briefing, scrap.count()) + .from(briefing) + .leftJoin(scrap).on(scrap.briefing.eq(briefing)) + .where(briefing.type.eq(type) + .and(briefing.createdAt.between(start, end)) + .and(briefing.timeOfDay.eq(timeOfDay)) + ) + .groupBy(briefing) + .orderBy(briefing.ranks.asc()) + .fetch(); + + // Tuple 결과를 Briefing과 scrapCount로 변환합니다. + return results.stream() + .map(tuple -> { + Briefing b = tuple.get(briefing); + b.setScrapCount(Math.toIntExact(tuple.get(scrap.count()))); + return b; + }) + .toList(); + } + + @Override + public List findTop10ByTypeOrderByCreatedAtDesc(BriefingType type) { + QBriefing briefing = QBriefing.briefing; + QScrap scrap = QScrap.scrap; + + List results = queryFactory.select(briefing, scrap.count()) + .from(briefing) + .leftJoin(scrap).on(scrap.briefing.eq(briefing)) + .where(briefing.type.eq(type)) + .groupBy(briefing) + .orderBy(briefing.createdAt.desc()) + .limit(10) + .fetch(); + + return results.stream() + .map(tuple -> { + Briefing b = tuple.get(briefing); + b.setScrapCount(Math.toIntExact(tuple.get(scrap.count()))); + return b; + }) + .collect(Collectors.toCollection(ArrayList::new)); + } + + + @Override + public Optional findByIdWithScrapCount(Long id) { + QBriefing briefing = QBriefing.briefing; + QScrap scrap = QScrap.scrap; + + // 쿼리 결과를 Tuple로 가져옵니다. + Tuple result = queryFactory.select(briefing, scrap.count()) + .from(briefing) + .leftJoin(scrap).on(scrap.briefing.eq(briefing)) + .where(briefing.id.eq(id)) + .groupBy(briefing) + .fetchOne(); // 단일 결과를 가져옵니다. + + // 결과가 없으면 Optional.empty() 반환 + if (result == null) { + return Optional.empty(); + } + + // Tuple 결과를 Briefing과 scrapCount로 변환합니다. + Briefing b = result.get(briefing); + b.setScrapCount(Math.toIntExact(result.get(scrap.count()))); + + return Optional.of(b); + } + +} diff --git a/src/main/java/briefing/briefing/domain/repository/BriefingRepository.java b/src/main/java/briefing/briefing/domain/repository/BriefingRepository.java index dc21463..1ccae49 100644 --- a/src/main/java/briefing/briefing/domain/repository/BriefingRepository.java +++ b/src/main/java/briefing/briefing/domain/repository/BriefingRepository.java @@ -8,7 +8,7 @@ import org.springframework.stereotype.Repository; @Repository -public interface BriefingRepository extends JpaRepository { +public interface BriefingRepository extends JpaRepository, BriefingCustomRepository { List findAllByTypeAndCreatedAtBetweenOrderByRanks(BriefingType type, LocalDateTime start, LocalDateTime end); diff --git a/src/main/java/briefing/chatting/domain/GptModel.java b/src/main/java/briefing/chatting/domain/GptModel.java index b8ab16f..1b34787 100644 --- a/src/main/java/briefing/chatting/domain/GptModel.java +++ b/src/main/java/briefing/chatting/domain/GptModel.java @@ -11,7 +11,8 @@ @Getter @RequiredArgsConstructor public enum GptModel { - GPT_3_5_TURBO("gpt-3.5-turbo"); + GPT_3_5_TURBO("gpt-3.5-turbo"), + GPT_4("gpt-4"); @JsonValue private final String value; diff --git a/src/main/java/briefing/common/enums/APIVersion.java b/src/main/java/briefing/common/enums/APIVersion.java new file mode 100644 index 0000000..4908e64 --- /dev/null +++ b/src/main/java/briefing/common/enums/APIVersion.java @@ -0,0 +1,30 @@ +package briefing.common.enums; + +import briefing.exception.ErrorCode; +import briefing.exception.handler.BriefingException; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +@AllArgsConstructor +public enum APIVersion { + V1("v1"), + V2("v2"); + + private final String version; + + public static APIVersion findByValue(String type) { + return Arrays.stream(values()) + .filter(value -> value.getVersion().equals(type)) + .findAny() + .orElseThrow(() -> new BriefingException(ErrorCode.NOT_FOUND_TYPE)); + } + + @JsonValue + String getAPIVersion() { + return this.getVersion(); + } +} diff --git a/src/main/java/briefing/config/GlobalWebConfig.java b/src/main/java/briefing/config/GlobalWebConfig.java index bfc619a..1623242 100644 --- a/src/main/java/briefing/config/GlobalWebConfig.java +++ b/src/main/java/briefing/config/GlobalWebConfig.java @@ -1,9 +1,6 @@ package briefing.config; -import briefing.converter.BriefingTypeRequestConverter; -import briefing.converter.GptModelRequestConverter; -import briefing.converter.MessageRoleRequestConverter; -import briefing.converter.SocialTypeRequestConverter; +import briefing.converter.*; import briefing.security.handler.annotation.AuthUserArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; @@ -29,11 +26,18 @@ public void addFormatters(final FormatterRegistry registry) { registry.addConverter(new GptModelRequestConverter()); registry.addConverter(new MessageRoleRequestConverter()); registry.addConverter(new SocialTypeRequestConverter()); + registry.addConverter(new TimeOfDayConverter()); + registry.addConverter(new APIVersionRequestConverter()); } @Override public void addCorsMappings(final CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000", "https://briefing-web.vercel.app","https://dev.newsbreifing.store"); + .allowedOrigins("*") + .allowedMethods("*") + .allowedHeaders("*") + .allowCredentials(false) + .maxAge(6000); + } } diff --git a/src/main/java/briefing/config/QueryDslConfig.java b/src/main/java/briefing/config/QueryDslConfig.java new file mode 100644 index 0000000..fded353 --- /dev/null +++ b/src/main/java/briefing/config/QueryDslConfig.java @@ -0,0 +1,20 @@ +package briefing.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class QueryDslConfig { + + + private final EntityManager em; + + @Bean + public JPAQueryFactory jpaQueryFactory(){ + return new JPAQueryFactory(em); + } +} \ No newline at end of file diff --git a/src/main/java/briefing/converter/APIVersionRequestConverter.java b/src/main/java/briefing/converter/APIVersionRequestConverter.java new file mode 100644 index 0000000..fab93fc --- /dev/null +++ b/src/main/java/briefing/converter/APIVersionRequestConverter.java @@ -0,0 +1,12 @@ +package briefing.converter; + +import briefing.common.enums.APIVersion; +import lombok.NonNull; +import org.springframework.core.convert.converter.Converter; +public class APIVersionRequestConverter implements Converter{ + + @Override + public APIVersion convert(@NonNull final String source) { + return APIVersion.findByValue(source); + } +} diff --git a/src/main/java/briefing/converter/TimeOfDayConverter.java b/src/main/java/briefing/converter/TimeOfDayConverter.java new file mode 100644 index 0000000..fa0116e --- /dev/null +++ b/src/main/java/briefing/converter/TimeOfDayConverter.java @@ -0,0 +1,12 @@ +package briefing.converter; + +import briefing.briefing.domain.TimeOfDay; +import lombok.NonNull; +import org.springframework.core.convert.converter.Converter; + +public class TimeOfDayConverter implements Converter { + @Override + public TimeOfDay convert(@NonNull final String source) { + return TimeOfDay.findByValue(source); + } +} diff --git a/src/main/java/briefing/exception/ErrorCode.java b/src/main/java/briefing/exception/ErrorCode.java index 8c2cd05..81f97ef 100644 --- a/src/main/java/briefing/exception/ErrorCode.java +++ b/src/main/java/briefing/exception/ErrorCode.java @@ -56,6 +56,7 @@ public enum ErrorCode { // scrap 에러 SCRAP_ALREADY_EXISTS(CONFLICT, "SCRAP_001", "이미 스크랩했습니다."), SCRAP_NOT_FOUND(NOT_FOUND, "SCRAP_002", "존재하지 않는 스크랩입니다."), + DUPLICATE_SCRAP(CONFLICT, "SCRAP_003", "중복된 스크랩 요청입니다."), // briefing 에러 NOT_FOUND_BRIEFING(NOT_FOUND,"BRIEFING_001", "브리핑이 존재하지 않습니다."), diff --git a/src/main/java/briefing/member/api/MemberApi.java b/src/main/java/briefing/member/api/MemberApi.java index 3809280..e19d8d7 100644 --- a/src/main/java/briefing/member/api/MemberApi.java +++ b/src/main/java/briefing/member/api/MemberApi.java @@ -33,7 +33,6 @@ @Tag(name = "02-Member \uD83D\uDC64",description = "사용자 관련 API") @RestController @Validated -@RequestMapping("/members") @RequiredArgsConstructor @ApiResponses({ @ApiResponse(responseCode = "COMMON000", description = "SERVER ERROR, 백앤드 개발자에게 알려주세요", content = @Content(schema = @Schema(implementation = CommonResponse.class))), @@ -47,7 +46,7 @@ public class MemberApi { private final RedisService redisService; @Operation(summary = "Member\uD83D\uDC64 토큰 잘 발급되나 테스트용API", description = "테스트 용") - @GetMapping("/auth/test") + @GetMapping("/members/auth/test") public CommonResponse testGenerateToken(){ Member member = memberQueryService.testForTokenApi(); String accessToken = tokenProvider.createAccessToken(member.getId(), member.getSocialType().toString() ,member.getSocialId(), Arrays.asList(new SimpleGrantedAuthority(member.getRole().name()))); @@ -55,27 +54,38 @@ public CommonResponse testGenerateToken(){ return CommonResponse.onSuccess(MemberConverter.toLoginDTO(member,accessToken, refreshToken.getToken())); } - @Operation(summary = "02-01 Member\uD83D\uDC64 소셜 로그인 #FRAME", description = "구글, 애플 소셜로그인 API입니다.") - @PostMapping("/auth/{socialType}") + @Operation(summary = "02-01 Member\uD83D\uDC64 소셜 로그인 V1", description = "구글, 애플 소셜로그인 API입니다.") + @PostMapping("/members/auth/{socialType}") public CommonResponse login( @Parameter(description = "소셜로그인 종류", example = "google") @PathVariable final SocialType socialType, @RequestBody final MemberRequest.LoginDTO request ) { Member member = memberCommandService.login(socialType, request); - // TODO - TokenProvider에서 발급해주도록 변경 String accessToken = tokenProvider.createAccessToken(member.getId(),member.getSocialType().toString() ,member.getSocialId(), List.of(new SimpleGrantedAuthority(MemberRole.ROLE_USER.name()))); String refreshToken = redisService.generateRefreshToken(member.getSocialId(),member.getSocialType()).getToken(); return CommonResponse.onSuccess(MemberConverter.toLoginDTO(member, accessToken, refreshToken)); } - @Operation(summary = "02-01 Member\uD83D\uDC64 accessToken 재발급 받기", description = "accessToken 만료 시 refreshToken으로 재발급을 받는 API 입니다.") + @Operation(summary = "02-01 Member\uD83D\uDC64 소셜 로그인 V2", description = "구글, 애플 소셜로그인 API입니다.") + @PostMapping("/v2/members/auth/{socialType}") + public CommonResponse loginV2( + @Parameter(description = "소셜로그인 종류", example = "google") @PathVariable final SocialType socialType, + @RequestBody final MemberRequest.LoginDTO request + ) { + Member member = memberCommandService.login(socialType, request); + String accessToken = tokenProvider.createAccessToken(member.getId(),member.getSocialType().toString() ,member.getSocialId(), List.of(new SimpleGrantedAuthority(MemberRole.ROLE_USER.name()))); + String refreshToken = redisService.generateRefreshToken(member.getSocialId(),member.getSocialType()).getToken(); + return CommonResponse.onSuccess(MemberConverter.toLoginDTO(member, accessToken, refreshToken)); + } + + @Operation(summary = "02-01 Member\uD83D\uDC64 accessToken 재발급 받기 V1", description = "accessToken 만료 시 refreshToken으로 재발급을 받는 API 입니다.") @ApiResponses({ @ApiResponse(responseCode = "1000",description = "OK, 성공"), @ApiResponse(responseCode = "COMMON001", description = "request body에 담길 값이 이상함, result를 확인해주세요!",content = @Content(schema = @Schema(implementation = CommonResponse.class))), @ApiResponse(responseCode = "AUTH005", description = "리프레시 토큰도 만료, 다시 로그인",content = @Content(schema = @Schema(implementation = CommonResponse.class))), @ApiResponse(responseCode = "AUTH009", description = "리프레시 토큰 모양이 잘못 됨",content = @Content(schema = @Schema(implementation = CommonResponse.class))), }) - @PostMapping("/auth/token") + @PostMapping("/members/auth/token") public CommonResponse reissueToken(@Valid @RequestBody MemberRequest.ReissueDTO request){ RefreshToken refreshToken = redisService.reGenerateRefreshToken(request); Member parsedMember = memberCommandService.parseRefreshToken(refreshToken); @@ -83,8 +93,23 @@ public CommonResponse reissueToken(@Valid @Reque return CommonResponse.onSuccess(MemberConverter.toReIssueTokenDTO(parsedMember.getId(), accessToken,refreshToken.getToken())); } - @Operation(summary = "02-01 Member\uD83D\uDC64 회원 탈퇴", description = "회원 탈퇴 API 입니다.") - @DeleteMapping("/{memberId}") + @Operation(summary = "02-01 Member\uD83D\uDC64 accessToken 재발급 받기 V2", description = "accessToken 만료 시 refreshToken으로 재발급을 받는 API 입니다.") + @ApiResponses({ + @ApiResponse(responseCode = "1000",description = "OK, 성공"), + @ApiResponse(responseCode = "COMMON001", description = "request body에 담길 값이 이상함, result를 확인해주세요!",content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "AUTH005", description = "리프레시 토큰도 만료, 다시 로그인",content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "AUTH009", description = "리프레시 토큰 모양이 잘못 됨",content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @PostMapping("/v2/members/auth/token") + public CommonResponse reissueTokenV2(@Valid @RequestBody MemberRequest.ReissueDTO request){ + RefreshToken refreshToken = redisService.reGenerateRefreshToken(request); + Member parsedMember = memberCommandService.parseRefreshToken(refreshToken); + String accessToken = tokenProvider.createAccessToken(parsedMember.getId(),parsedMember.getSocialType().toString(), parsedMember.getSocialId(), List.of(new SimpleGrantedAuthority(parsedMember.getRole().toString()))); + return CommonResponse.onSuccess(MemberConverter.toReIssueTokenDTO(parsedMember.getId(), accessToken,refreshToken.getToken())); + } + + @Operation(summary = "02-01 Member\uD83D\uDC64 회원 탈퇴 V1", description = "회원 탈퇴 API 입니다.") + @DeleteMapping("/members/{memberId}") @Parameters({ @Parameter(name = "member", hidden = true), @Parameter(name = "memberId", description = "삭제 대상 멤버아이디") @@ -101,4 +126,23 @@ public CommonResponse quitMember(@AuthMember Member memb memberCommandService.deleteMember(memberId); return CommonResponse.onSuccess(MemberConverter.toQuitDTO()); } + + @Operation(summary = "02-01 Member\uD83D\uDC64 회원 탈퇴 V2", description = "회원 탈퇴 API 입니다.") + @DeleteMapping("/v2/members/{memberId}") + @Parameters({ + @Parameter(name = "member", hidden = true), + @Parameter(name = "memberId", description = "삭제 대상 멤버아이디") + }) + @ApiResponses({ + @ApiResponse(responseCode = "1000",description = "OK, 성공"), + @ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!",content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "AUTH004", description = "acess 토큰 만료",content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "AUTH006", description = "acess 토큰 모양이 이상함",content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "MEMBER_001", description = "사용자가 존재하지 않습니다.",content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "MEMBER_002", description = "로그인 한 사용자와 탈퇴 대상이 동일하지 않습니다.",content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + public CommonResponse quitMemberV2(@AuthMember Member member, @CheckSameMember @PathVariable Long memberId){ + memberCommandService.deleteMember(memberId); + return CommonResponse.onSuccess(MemberConverter.toQuitDTO()); + } } diff --git a/src/main/java/briefing/member/domain/Member.java b/src/main/java/briefing/member/domain/Member.java index 071d8e2..93e6722 100644 --- a/src/main/java/briefing/member/domain/Member.java +++ b/src/main/java/briefing/member/domain/Member.java @@ -27,13 +27,16 @@ public class Member extends BaseDateTimeEntity { @Enumerated(EnumType.STRING) private SocialType socialType; + @Builder.Default @Enumerated(EnumType.STRING) private MemberStatus status = MemberStatus.INACTIVE; + @Builder.Default @Enumerated(EnumType.STRING) private MemberRole role = MemberRole.ROLE_USER; // cascade 설정 + @Builder.Default @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List scrapList = new ArrayList<>(); } diff --git a/src/main/java/briefing/scrap/api/ScrapApi.java b/src/main/java/briefing/scrap/api/ScrapApi.java index 3761c4a..3f91143 100644 --- a/src/main/java/briefing/scrap/api/ScrapApi.java +++ b/src/main/java/briefing/scrap/api/ScrapApi.java @@ -15,30 +15,51 @@ @Tag(name = "05-Scrap 📁", description = "스크랩 관련 API") @RestController -@RequestMapping("/scraps") @RequiredArgsConstructor public class ScrapApi { private final ScrapQueryService scrapQueryService; private final ScrapCommandService scrapCommandService; - @Operation(summary = "05-01 Scrap📁 스크랩하기 #FRAME", description = "브리핑을 스크랩하는 API입니다.") - @PostMapping("/briefings") + @Operation(summary = "05-01 Scrap📁 스크랩하기 V1", description = "브리핑을 스크랩하는 API입니다.") + @PostMapping("/scraps/briefings") public CommonResponse create(@RequestBody ScrapRequest.CreateDTO request) { Scrap createdScrap = scrapCommandService.create(request); return CommonResponse.onSuccess(ScrapConverter.toCreateDTO(createdScrap)); } - @Operation(summary = "05-02 Scrap📁 스크랩 취소 #FRAME", description = "스크랩을 취소하는 API입니다.") - @DeleteMapping("/briefings/{briefingId}/members/{memberId}") + @Operation(summary = "05-01 Scrap📁 스크랩하기 V2", description = "브리핑을 스크랩하는 API입니다.") + @PostMapping("/v2/scraps/briefings") + public CommonResponse createV2(@RequestBody ScrapRequest.CreateDTO request) { + Scrap createdScrap = scrapCommandService.create(request); + return CommonResponse.onSuccess(ScrapConverter.toCreateDTO(createdScrap)); + } + + @Operation(summary = "05-02 Scrap📁 스크랩 취소 V1", description = "스크랩을 취소하는 API입니다.") + @DeleteMapping("/scraps/briefings/{briefingId}/members/{memberId}") public CommonResponse delete(@PathVariable Long briefingId, @PathVariable Long memberId) { Scrap deletedScrap = scrapCommandService.delete(briefingId, memberId); return CommonResponse.onSuccess(ScrapConverter.toDeleteDTO(deletedScrap)); } - @Operation(summary = "05-03 Scrap📁 내 스크랩 조회 #FRAME", description = "내 스크랩을 조회하는 API입니다.") - @GetMapping("/briefings/members/{memberId}") + @Operation(summary = "05-02 Scrap📁 스크랩 취소 V2", description = "스크랩을 취소하는 API입니다.") + @DeleteMapping("/v2/scraps/briefings/{briefingId}/members/{memberId}") + public CommonResponse deleteV2(@PathVariable Long briefingId, @PathVariable Long memberId) { + Scrap deletedScrap = scrapCommandService.delete(briefingId, memberId); + return CommonResponse.onSuccess(ScrapConverter.toDeleteDTO(deletedScrap)); + } + + + @Operation(summary = "05-03 Scrap📁 내 스크랩 조회 V1", description = "내 스크랩을 조회하는 API입니다.") + @GetMapping("/scraps/briefings/members/{memberId}") public CommonResponse> getScrapsByMember(@PathVariable Long memberId) { List scraps = scrapQueryService.getScrapsByMemberId(memberId); return CommonResponse.onSuccess(scraps.stream().map(ScrapConverter::toReadDTO).toList()); } + + @Operation(summary = "05-03 Scrap📁 내 스크랩 조회 V2", description = "내 스크랩을 조회하는 API입니다.") + @GetMapping("/v2/scraps/briefings/members/{memberId}") + public CommonResponse> getScrapsByMemberV2(@PathVariable Long memberId) { + List scraps = scrapQueryService.getScrapsByMemberId(memberId); + return CommonResponse.onSuccess(scraps.stream().map(ScrapConverter::toReadDTOV2).toList()); + } } diff --git a/src/main/java/briefing/scrap/api/ScrapConverter.java b/src/main/java/briefing/scrap/api/ScrapConverter.java index 8cbab78..d55d031 100644 --- a/src/main/java/briefing/scrap/api/ScrapConverter.java +++ b/src/main/java/briefing/scrap/api/ScrapConverter.java @@ -40,4 +40,16 @@ public static ScrapResponse.ReadDTO toReadDTO(Scrap scrap) { .date(scrap.getBriefing().getCreatedAt().toLocalDate()) .build(); } + + public static ScrapResponse.ReadDTOV2 toReadDTOV2(Scrap scrap) { + return ScrapResponse.ReadDTOV2.builder() + .briefingId(scrap.getBriefing().getId()) + .ranks(scrap.getBriefing().getRanks()) + .title(scrap.getBriefing().getTitle()) + .subtitle(scrap.getBriefing().getSubtitle()) + .date(scrap.getBriefing().getCreatedAt().toLocalDate()) + .gptModel(scrap.getBriefing().getGptModel()) + .timeOfDay(scrap.getBriefing().getTimeOfDay()) + .build(); + } } diff --git a/src/main/java/briefing/scrap/application/ScrapCommandService.java b/src/main/java/briefing/scrap/application/ScrapCommandService.java index bcbea3c..49e0849 100644 --- a/src/main/java/briefing/scrap/application/ScrapCommandService.java +++ b/src/main/java/briefing/scrap/application/ScrapCommandService.java @@ -13,6 +13,7 @@ import briefing.scrap.domain.repository.ScrapRepository; import briefing.scrap.exception.ScrapException; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -40,7 +41,13 @@ public Scrap create(ScrapRequest.CreateDTO request) { Scrap scrap = ScrapConverter.toScrap(member, briefing); // Scrap 엔티티 저장 및 반환 - return scrapRepository.save(scrap); + try { + // Scrap 엔티티 저장 및 반환 + return scrapRepository.save(scrap); + } catch (DataIntegrityViolationException e) { + // 중복 스크랩 예외 처리 + throw new ScrapException(ErrorCode.DUPLICATE_SCRAP); + } } public Scrap delete(Long briefingId, Long memberId) { diff --git a/src/main/java/briefing/scrap/application/dto/ScrapResponse.java b/src/main/java/briefing/scrap/application/dto/ScrapResponse.java index 27fcf50..2b4a0ec 100644 --- a/src/main/java/briefing/scrap/application/dto/ScrapResponse.java +++ b/src/main/java/briefing/scrap/application/dto/ScrapResponse.java @@ -1,5 +1,7 @@ package briefing.scrap.application.dto; +import briefing.briefing.domain.TimeOfDay; +import briefing.chatting.domain.GptModel; import jakarta.persistence.Column; import lombok.AllArgsConstructor; import lombok.Builder; @@ -31,7 +33,6 @@ public static class DeleteDTO { private LocalDateTime deletedAt; } - @Builder @Getter @NoArgsConstructor @@ -43,4 +44,20 @@ public static class ReadDTO { private String subtitle; private LocalDate date; } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class ReadDTOV2 { + private Long briefingId; + private Integer ranks; + private String title; + private String subtitle; + private LocalDate date; + @Builder.Default + private GptModel gptModel = GptModel.GPT_3_5_TURBO; + @Builder.Default + private TimeOfDay timeOfDay = TimeOfDay.MORNING; + } } diff --git a/src/main/java/briefing/scrap/domain/Scrap.java b/src/main/java/briefing/scrap/domain/Scrap.java index 5063019..08d3a9d 100644 --- a/src/main/java/briefing/scrap/domain/Scrap.java +++ b/src/main/java/briefing/scrap/domain/Scrap.java @@ -10,6 +10,9 @@ @Getter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor +@Table(uniqueConstraints = { + @UniqueConstraint(columnNames = {"member_id", "briefing_id"}) +}) public class Scrap extends BaseDateTimeEntity { @Id diff --git a/src/main/java/briefing/security/config/SecurityConfig.java b/src/main/java/briefing/security/config/SecurityConfig.java index 8f3bb4e..d16a5b3 100644 --- a/src/main/java/briefing/security/config/SecurityConfig.java +++ b/src/main/java/briefing/security/config/SecurityConfig.java @@ -70,7 +70,7 @@ public WebSecurityCustomizer webSecurityCustomizer() { "/v3/api-docs/**", "/swagger-ui/index.html", "/swagger-ui/**", - "/docs/**"); + "/docs/**","/briefings/temp"); } @Bean @@ -82,10 +82,14 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .sessionManagement(manage -> manage.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Session 사용 안함 .formLogin(AbstractHttpConfigurer::disable) // form login 사용 안함 .authorizeHttpRequests(authorize -> { + authorize.requestMatchers("/v2/briefings/**").permitAll(); // 모두 접근 가능합니다. authorize.requestMatchers("/briefings/**").permitAll(); // 모두 접근 가능합니다. + authorize.requestMatchers("/v2/members/auth/**").permitAll(); authorize.requestMatchers("/members/auth/**").permitAll(); authorize.requestMatchers("/chattings/**").permitAll(); + authorize.requestMatchers(HttpMethod.DELETE, "/v2/members/{memberId}").authenticated(); authorize.requestMatchers(HttpMethod.DELETE, "/members/{memberId}").authenticated(); + authorize.requestMatchers("/v2/scraps/**").authenticated(); authorize.requestMatchers("/scraps/**").authenticated(); authorize.anyRequest().authenticated(); }) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9659f48..16dff82 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,5 +1,7 @@ # default profile spring: + application: + name: briefing-dev profiles: active: dev springdoc: @@ -9,7 +11,6 @@ springdoc: cache: disabled: true use-fqn: true - --- spring: config: @@ -27,8 +28,8 @@ spring: properties: hibernate: dialect: org.hibernate.dialect.MySQLDialect - # show_sql: true - # format_sql: true + show_sql: true + format_sql: true use_sql_comments: true hbm2ddl: auto: update @@ -38,6 +39,16 @@ spring: redis: host: localhost port: 6379 +eureka: + client: + service-url: + defaultZone: http://gateway.newsbreifing.store:8761/eureka + instance: + # 표기되는 규칙 변경 + instance-id: dev_briefing_server + hostname: dev.newsbreifing.store + ip-address: dev.newsbreifing.store + jwt: header: Authorization # dev server @@ -53,6 +64,8 @@ openai: --- spring: + application: + name: dev config: activate: on-profile: dev @@ -76,8 +89,18 @@ spring: default_batch_fetch_size: 1000 data: redis: - host: breifing-redis-cluster.bjyb5r.ng.0001.apne1.cache.amazonaws.com + host: ${REDIS_URL} port: 6379 +eureka: + instance: + # 표기되는 규칙 변경 + instance-id: dev_briefing_server + hostname: dev.newsbreifing.store + ip-address: dev.newsbreifing.store + client: + service-url: + defaultZone: http://gateway.newsbreifing.store:8761/eureka + jwt: header: Authorization # dev server @@ -91,7 +114,6 @@ openai: token: ${OPEN_API_TOKEN} url: chat: https://api.openai.com/v1/chat/completions - --- spring: config: @@ -117,7 +139,7 @@ spring: default_batch_fetch_size: 1000 data: redis: - host: briefing-release-redis.0ot2cs.ng.0001.apn2.cache.amazonaws.com + host: ${release.redis.host} port: 6379 jwt: header: Authorization