diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000000..661947471f --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,83 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created +# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle + +name: Java CI with Gradle + +on: + push: + branches: [ BE-deploy ] + pull_request: + branches: [ BE-deploy ] + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./BE + + steps: + - uses: actions/checkout@v2 + + # JDK11로 gradle 빌드 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'temurin' + + - name: Insert data source information into application.yml + run: | + sed -i "s|\${DATASOURCE_URL}|$DATASOURCE_URL|g" ./src/main/resources/application.yml + sed -i "s|\${DATASOURCE_USERNAME}|$DATASOURCE_USERNAME|g" ./src/main/resources/application.yml + sed -i "s|\${DATASOURCE_PASSWORD}|$DATASOURCE_PASSWORD|g" ./src/main/resources/application.yml + + sed -i "s|\${OAUTH_CLIENT_ID}|$OAUTH_CLIENT_ID|g" ./src/main/resources/oauth.yml + sed -i "s|\${OAUTH_CLIENT_SECRET}|$OAUTH_CLIENT_SECRET|g" ./src/main/resources/oauth.yml + sed -i "s|\${OAUTH_ACCESS_SCOPE}|$OAUTH_ACCESS_SCOPE|g" ./src/main/resources/oauth.yml + sed -i "s|\${OAUTH_LOGIN_FORM_URL}|$OAUTH_LOGIN_FORM_URL|g" ./src/main/resources/oauth.yml + sed -i "s|\${JWT_ISSUER}|$JWT_ISSUER|g" ./src/main/resources/oauth.yml + sed -i "s|\${JWT_SECRET_KEY}|$JWT_SECRET_KEY|g" ./src/main/resources/oauth.yml + + env: + DATASOURCE_URL: ${{ secrets.DATASOURCE_URL }} + DATASOURCE_USERNAME: ${{ secrets.DATASOURCE_USERNAME }} + DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} + OAUTH_CLIENT_ID: ${{ secrets.OAUTH_CLIENT_ID }} + OAUTH_CLIENT_SECRET: ${{ secrets.OAUTH_CLIENT_SECRET }} + OAUTH_ACCESS_SCOPE: ${{ secrets.OAUTH_ACCESS_SCOPE }} + OAUTH_LOGIN_FORM_URL: ${{ secrets.OAUTH_LOGIN_FORM_URL }} + JWT_ISSUER: ${{ secrets.JWT_ISSUER }} + JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build + + # 도커 빌드(도커 이미지 생성) + - name: Docker build + run: | + docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + docker build -t ${{ secrets.DOCKER_USERNAME }}/issue-tracker:1.0 . + docker push ${{ secrets.DOCKER_USERNAME }}/issue-tracker:1.0 + docker rmi ${{ secrets.DOCKER_USERNAME }}/issue-tracker:1.0 + + # 도커 이미지 배포 및 실행(EC2 ubuntu20.04로 배포) + - name: Deploy + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.AWS_HOST_DNS }} #(생성한 인스턴스의 DNS주소) + username: ubuntu + key: ${{ secrets.SSH_PRIVATE_KEY }} #(인스턴스를 생성할 때 다운받은 pem키안에 값을 모두 복사해서 붙여넣기) + envs: GITHUB_SHA + script: | + sudo docker ps -a -q --filter "name=issue-tracker" | grep -q . && docker stop issue-tracker && docker rm issue-tracker | true + sudo docker rmi ${{ secrets.DOCKER_USERNAME }}/issue-tracker:1.0 + sudo docker pull ${{ secrets.DOCKER_USERNAME }}/issue-tracker:1.0 + sudo docker run -d -p 80:8080 --name issue-tracker -e FILE_UPLOAD_S3_ACCESS_KEY=${{ secrets.FILE_UPLOAD_S3_ACCESS_KEY }} -e FILE_UPLOAD_S3_SECRET_KEY=${{ secrets.FILE_UPLOAD_S3_SECRET_KEY }} ${{ secrets.DOCKER_USERNAME }}/issue-tracker:1.0 + sudo docker rmi -f $(docker images -f "dangling=true" -q) || true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..485dee64bc --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/BE/.gitignore b/BE/.gitignore index 07b1844496..24e0bd6501 100644 --- a/BE/.gitignore +++ b/BE/.gitignore @@ -1,4 +1,3 @@ -oauth.yml HELP.md .gradle build/ diff --git a/BE/Dockerfile b/BE/Dockerfile new file mode 100644 index 0000000000..3fdcbc1275 --- /dev/null +++ b/BE/Dockerfile @@ -0,0 +1,17 @@ +# JDK11 이미지 사용 +FROM openjdk:11-jdk + +#마운트에 /tmp를 사용하는 이유 +#spring boot의 Tomcat의 default 저장소가 /tmp인데 +#위와 같이 볼륨 마운트를 해주면 호스트의 /var/lib/docker에 임시파일을 만들고 +#컨테이너 안의 /tmp 와 연결할 수 있다는 뜻입니다. +VOLUME /tmp + +# JAR_FILE 변수에 값을 저장 +ARG JAR_FILE=./build/libs/*.jar + +# 변수에 저장된 것을 컨테이너 실행시 이름을 app.jar파일로 변경하여 컨테이너에 저장 +COPY ${JAR_FILE} app.jar + +# 빌드된 이미지가 run 될 때 실행할 명령어 +ENTRYPOINT ["java","-jar","/app.jar"] diff --git a/BE/build.gradle b/BE/build.gradle index 555553e7b5..97302c18fe 100644 --- a/BE/build.gradle +++ b/BE/build.gradle @@ -1,6 +1,13 @@ +buildscript { + ext { + queryDslVersion = "5.0.0" + } +} + plugins { id 'org.springframework.boot' version '2.7.0' id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" id 'java' } @@ -12,6 +19,7 @@ configurations { compileOnly { extendsFrom annotationProcessor } + querydsl.extendsFrom compileClasspath } repositories { @@ -24,14 +32,41 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.0' + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' implementation 'com.auth0:java-jwt:3.19.1' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'mysql:mysql-connector-java' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation "com.querydsl:querydsl-jpa:${queryDslVersion}" + annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}" } tasks.named('test') { useJUnitPlatform() } + +jar { + enabled = false +} +bootJar { + archivesBaseName = 'app' + archiveFileName = 'app.jar' + archiveVersion = "0.0.0" +} + +def querydslDir = "$buildDir/generated/querydsl" + +querydsl { + jpa = true + querydslSourcesDir = querydslDir +} + +sourceSets { + main.java.srcDir querydslDir +} + +compileQuerydsl { + options.annotationProcessorPath = configurations.querydsl +} diff --git a/BE/gradlew b/BE/gradlew old mode 100644 new mode 100755 diff --git a/BE/src/main/java/louie/hanse/issuetracker/config/QuerydslConfig.java b/BE/src/main/java/louie/hanse/issuetracker/config/QuerydslConfig.java new file mode 100644 index 0000000000..1859e49aa2 --- /dev/null +++ b/BE/src/main/java/louie/hanse/issuetracker/config/QuerydslConfig.java @@ -0,0 +1,19 @@ +package louie.hanse.issuetracker.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuerydslConfig { + + @PersistenceContext + EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/BE/src/main/java/louie/hanse/issuetracker/config/WebConfig.java b/BE/src/main/java/louie/hanse/issuetracker/config/WebConfig.java new file mode 100644 index 0000000000..caa9558053 --- /dev/null +++ b/BE/src/main/java/louie/hanse/issuetracker/config/WebConfig.java @@ -0,0 +1,25 @@ +package louie.hanse.issuetracker.config; + +import louie.hanse.issuetracker.converter.StringToStatusConverter; +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +//@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("GET", "POST", "PUT", "PATCH", "OPTIONS") + .allowedHeaders("headers"); + } + + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new StringToStatusConverter()); + } +} diff --git a/BE/src/main/java/louie/hanse/issuetracker/converter/StringToStatusConverter.java b/BE/src/main/java/louie/hanse/issuetracker/converter/StringToStatusConverter.java new file mode 100644 index 0000000000..0c37a8be19 --- /dev/null +++ b/BE/src/main/java/louie/hanse/issuetracker/converter/StringToStatusConverter.java @@ -0,0 +1,11 @@ +package louie.hanse.issuetracker.converter; + +import louie.hanse.issuetracker.domain.Status; +import org.springframework.core.convert.converter.Converter; + +public class StringToStatusConverter implements Converter { + @Override + public Status convert(String source) { + return Status.valueOfWithCaseInsensitive(source); + } +} diff --git a/BE/src/main/java/louie/hanse/issuetracker/domain/Comment.java b/BE/src/main/java/louie/hanse/issuetracker/domain/Comment.java index dc4cef5f6d..6ce548a62b 100644 --- a/BE/src/main/java/louie/hanse/issuetracker/domain/Comment.java +++ b/BE/src/main/java/louie/hanse/issuetracker/domain/Comment.java @@ -1,24 +1,28 @@ package louie.hanse.issuetracker.domain; -import javax.persistence.*; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - import static javax.persistence.FetchType.LAZY; -import static javax.persistence.GenerationType.*; +import static javax.persistence.GenerationType.IDENTITY; +import java.time.LocalDateTime; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter @Entity @Table(name = "comments") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Comment { @Id @GeneratedValue(strategy = IDENTITY) private Long id; - @OneToMany(mappedBy = "comment") - private List uploadFiles = new ArrayList<>(); - @ManyToOne(fetch = LAZY) @JoinColumn private Issue issue; @@ -27,8 +31,18 @@ public class Comment { @JoinColumn private Member writer; - private String content; - private LocalDateTime createdDateTime; - private LocalDateTime updatedDateTime; + private String contents; + private LocalDateTime createdDateTime = LocalDateTime.now(); + private LocalDateTime updatedDateTime = LocalDateTime.now(); + + public Comment(Issue issue, String contents) { + this.issue = issue; + issue.addComment(this); + this.contents = contents; + } + public void updateContents(String contents) { + this.contents = contents; + updatedDateTime = LocalDateTime.now(); + } } diff --git a/BE/src/main/java/louie/hanse/issuetracker/domain/Issue.java b/BE/src/main/java/louie/hanse/issuetracker/domain/Issue.java index 29c96aa4d6..dffdd9252a 100644 --- a/BE/src/main/java/louie/hanse/issuetracker/domain/Issue.java +++ b/BE/src/main/java/louie/hanse/issuetracker/domain/Issue.java @@ -1,5 +1,9 @@ package louie.hanse.issuetracker.domain; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + import javax.persistence.*; import java.time.LocalDateTime; @@ -9,19 +13,21 @@ import static javax.persistence.FetchType.LAZY; import static javax.persistence.GenerationType.IDENTITY; +@Getter @Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Issue { @Id @GeneratedValue(strategy = IDENTITY) private Long id; - @OneToMany(mappedBy = "issue") + @OneToMany(mappedBy = "issue", cascade = CascadeType.ALL, orphanRemoval = true) private List comments = new ArrayList<>(); - @OneToMany(mappedBy = "issue") + @OneToMany(mappedBy = "issue", cascade = CascadeType.ALL, orphanRemoval = true) private List issueLabels = new ArrayList<>(); - @ManyToOne(fetch = LAZY) + @ManyToOne(fetch = LAZY, cascade = CascadeType.PERSIST) @JoinColumn private Milestone milestone; @@ -29,13 +35,54 @@ public class Issue { @JoinColumn private Member writer; - @OneToMany(mappedBy = "issue") + @OneToMany(mappedBy = "issue", cascade = CascadeType.ALL, orphanRemoval = true) private List issueManagers = new ArrayList<>(); private String title; - private LocalDateTime createDateTime; + private LocalDateTime createDateTime = LocalDateTime.now(); @Enumerated(EnumType.STRING) - private Status status; + private Status status = Status.OPEN; + + public Issue(String title, Member writer) { + this.title = title; + this.writer = writer; + } + + public void updateStatus(Status status) { + this.status = status; + } + + public void updateTitle(String title) { + this.title = title; + } + + public void updateMilestone(Milestone milestone) { + this.milestone = milestone; + milestone.addIssue(this); + } + + public void addComment(Comment comment) { + this.comments.add(comment); + } + + public void addIssueManager(IssueManager issueManager) { + this.issueManagers.add(issueManager); + } + + public void addIssueLabel(IssueLabel issueLabel) { + this.issueLabels.add(issueLabel); + } + + public boolean isClosed() { + return status.isClosed(); + } + + public boolean isOpened() { + return status.isOpened(); + } + public void deleteMilestone() { + this.milestone = null; + } } diff --git a/BE/src/main/java/louie/hanse/issuetracker/domain/IssueLabel.java b/BE/src/main/java/louie/hanse/issuetracker/domain/IssueLabel.java index 3e9bbd3b16..e7a5b0f937 100644 --- a/BE/src/main/java/louie/hanse/issuetracker/domain/IssueLabel.java +++ b/BE/src/main/java/louie/hanse/issuetracker/domain/IssueLabel.java @@ -1,11 +1,16 @@ package louie.hanse.issuetracker.domain; +import lombok.Getter; +import lombok.NoArgsConstructor; + import javax.persistence.*; import static javax.persistence.FetchType.LAZY; import static javax.persistence.GenerationType.IDENTITY; +@Getter @Entity +@NoArgsConstructor public class IssueLabel { @Id @@ -19,4 +24,10 @@ public class IssueLabel { @ManyToOne(fetch = LAZY) @JoinColumn private Label label; + + public IssueLabel(Issue issue, Label label) { + this.issue = issue; + issue.addIssueLabel(this); + this.label = label; + } } diff --git a/BE/src/main/java/louie/hanse/issuetracker/domain/IssueManager.java b/BE/src/main/java/louie/hanse/issuetracker/domain/IssueManager.java index 256ca7794c..b5439487bb 100644 --- a/BE/src/main/java/louie/hanse/issuetracker/domain/IssueManager.java +++ b/BE/src/main/java/louie/hanse/issuetracker/domain/IssueManager.java @@ -1,10 +1,16 @@ package louie.hanse.issuetracker.domain; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + import javax.persistence.*; import static javax.persistence.GenerationType.IDENTITY; +@Getter @Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class IssueManager { @Id @GeneratedValue(strategy = IDENTITY) @@ -16,5 +22,11 @@ public class IssueManager { @ManyToOne @JoinColumn - private Member manger; + private Member manager; + + public IssueManager(Issue issue, Member manager) { + this.issue = issue; + issue.addIssueManager(this); + this.manager = manager; + } } diff --git a/BE/src/main/java/louie/hanse/issuetracker/domain/Label.java b/BE/src/main/java/louie/hanse/issuetracker/domain/Label.java index 039c8a5cd8..55218dd4f1 100644 --- a/BE/src/main/java/louie/hanse/issuetracker/domain/Label.java +++ b/BE/src/main/java/louie/hanse/issuetracker/domain/Label.java @@ -2,18 +2,30 @@ import java.util.ArrayList; import java.util.List; -import javax.persistence.*; +import javax.persistence.CascadeType; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.OneToMany; +import lombok.AccessLevel; +import lombok.Getter; -import static javax.persistence.FetchType.LAZY; -import static javax.persistence.GenerationType.*; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import lombok.NoArgsConstructor; +import static javax.persistence.GenerationType.IDENTITY; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter @Entity public class Label { @Id @GeneratedValue(strategy = IDENTITY) private Long id; - @OneToMany(mappedBy = "issue") + @OneToMany(mappedBy = "label", cascade = CascadeType.REMOVE) private List issueLabels = new ArrayList<>(); @Column(name = "label_name") @@ -22,6 +34,31 @@ public class Label { @Column(name = "label_description") private String description; private String backgroundColor; - private String textColor; + @Enumerated(EnumType.STRING) + private TextColor textColor; + + public Label(String name, String description, String backgroundColor, + TextColor textColor) { + this.name = name; + this.description = description; + this.backgroundColor = backgroundColor; + this.textColor = textColor; + } + + public void changeName(String name) { + this.name = name; + } + + public void changeDescription(String description) { + this.description = description; + } + + public void changeBackgroundColor(String backgroundColor) { + this.backgroundColor = backgroundColor; + } + + public void changeTextColor(TextColor textColor) { + this.textColor = textColor; + } } diff --git a/BE/src/main/java/louie/hanse/issuetracker/domain/Member.java b/BE/src/main/java/louie/hanse/issuetracker/domain/Member.java index 9271ff9aa5..1595d7e2c6 100644 --- a/BE/src/main/java/louie/hanse/issuetracker/domain/Member.java +++ b/BE/src/main/java/louie/hanse/issuetracker/domain/Member.java @@ -1,6 +1,7 @@ package louie.hanse.issuetracker.domain; import lombok.AccessLevel; +import lombok.Getter; import lombok.NoArgsConstructor; import javax.persistence.Entity; @@ -9,6 +10,7 @@ import static javax.persistence.GenerationType.IDENTITY; +@Getter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Member { @@ -25,15 +27,12 @@ public Member(String socialId, String avatarImageUrl) { this.avatarImageUrl = avatarImageUrl; } - public Long getId() { - return id; + public void changeRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; } - public String getRefreshToken() { - return refreshToken; + public void deleteRefreshToken() { + this.refreshToken = null; } - public void changeRefreshToken(String refreshToken) { - this.refreshToken = refreshToken; - } } diff --git a/BE/src/main/java/louie/hanse/issuetracker/domain/Milestone.java b/BE/src/main/java/louie/hanse/issuetracker/domain/Milestone.java index 1cc068c9d5..4122f84a0e 100644 --- a/BE/src/main/java/louie/hanse/issuetracker/domain/Milestone.java +++ b/BE/src/main/java/louie/hanse/issuetracker/domain/Milestone.java @@ -1,5 +1,9 @@ package louie.hanse.issuetracker.domain; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + import javax.persistence.*; import java.time.LocalDate; import java.util.ArrayList; @@ -7,14 +11,16 @@ import static javax.persistence.GenerationType.*; +@Getter @Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Milestone { @Id @GeneratedValue(strategy = IDENTITY) private Long id; @OneToMany(mappedBy = "milestone") - private List issue = new ArrayList<>(); + private List issues = new ArrayList<>(); private String title; @@ -22,6 +28,25 @@ public class Milestone { private String description; private LocalDate completedDate; - @Enumerated(EnumType.STRING) - private Status status; + public void addIssue(Issue issue) { + this.issues.add(issue); + } + + public Milestone(String title, String description, LocalDate completedDate) { + this.title = title; + this.description = description; + this.completedDate = completedDate; + } + + public void updateTitle(String title) { + this.title = title; + } + + public void updateDescription(String description) { + this.description = description; + } + + public void updateCompletedDate(LocalDate completedDate) { + this.completedDate = completedDate; + } } diff --git a/BE/src/main/java/louie/hanse/issuetracker/domain/Status.java b/BE/src/main/java/louie/hanse/issuetracker/domain/Status.java index edbac51f4f..156c8e3a18 100644 --- a/BE/src/main/java/louie/hanse/issuetracker/domain/Status.java +++ b/BE/src/main/java/louie/hanse/issuetracker/domain/Status.java @@ -1,5 +1,25 @@ package louie.hanse.issuetracker.domain; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum Status { - OPEN, CLOSE + OPEN, CLOSE; + + @JsonCreator + public static Status valueOfWithCaseInsensitive(String name) { + name = name.toUpperCase(); + return valueOf(name); + } + + public Status reverse() { + return this == OPEN ? CLOSE : OPEN; + } + + public boolean isClosed() { + return this.equals(CLOSE); + } + + public boolean isOpened() { + return this.equals(OPEN); + } } diff --git a/BE/src/main/java/louie/hanse/issuetracker/domain/TextColor.java b/BE/src/main/java/louie/hanse/issuetracker/domain/TextColor.java new file mode 100644 index 0000000000..0222217ca3 --- /dev/null +++ b/BE/src/main/java/louie/hanse/issuetracker/domain/TextColor.java @@ -0,0 +1,13 @@ +package louie.hanse.issuetracker.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public enum TextColor { + DARK, BRIGHT; + + @JsonCreator + public static TextColor valueOfWithCaseInsensitive(String name) { + name = name.toUpperCase(); + return valueOf(name); + } +} diff --git a/BE/src/main/java/louie/hanse/issuetracker/domain/UploadFile.java b/BE/src/main/java/louie/hanse/issuetracker/domain/UploadFile.java deleted file mode 100644 index 7c5cf1d005..0000000000 --- a/BE/src/main/java/louie/hanse/issuetracker/domain/UploadFile.java +++ /dev/null @@ -1,21 +0,0 @@ -package louie.hanse.issuetracker.domain; - -import javax.persistence.*; - -import static javax.persistence.FetchType.LAZY; -import static javax.persistence.GenerationType.IDENTITY; - -@Entity -public class UploadFile { - - @Id @GeneratedValue(strategy = IDENTITY) - private Long id; - - @ManyToOne(fetch = LAZY) - @JoinColumn - private Comment comment; - - private String originalFilename; - private String storeFilename; - private String url; -} diff --git a/BE/src/main/java/louie/hanse/issuetracker/jwt/JwtProperties.java b/BE/src/main/java/louie/hanse/issuetracker/login/jwt/JwtProperties.java similarity index 90% rename from BE/src/main/java/louie/hanse/issuetracker/jwt/JwtProperties.java rename to BE/src/main/java/louie/hanse/issuetracker/login/jwt/JwtProperties.java index 9673d5fce1..857dea5f4e 100644 --- a/BE/src/main/java/louie/hanse/issuetracker/jwt/JwtProperties.java +++ b/BE/src/main/java/louie/hanse/issuetracker/login/jwt/JwtProperties.java @@ -1,4 +1,4 @@ -package louie.hanse.issuetracker.jwt; +package louie.hanse.issuetracker.login.jwt; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/BE/src/main/java/louie/hanse/issuetracker/jwt/JwtProvider.java b/BE/src/main/java/louie/hanse/issuetracker/login/jwt/JwtProvider.java similarity index 90% rename from BE/src/main/java/louie/hanse/issuetracker/jwt/JwtProvider.java rename to BE/src/main/java/louie/hanse/issuetracker/login/jwt/JwtProvider.java index 589aadba5d..3f7101a936 100644 --- a/BE/src/main/java/louie/hanse/issuetracker/jwt/JwtProvider.java +++ b/BE/src/main/java/louie/hanse/issuetracker/login/jwt/JwtProvider.java @@ -1,4 +1,4 @@ -package louie.hanse.issuetracker.jwt; +package louie.hanse.issuetracker.login.jwt; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; @@ -58,6 +58,11 @@ private String createToken(String subject, Long memberId, Date expiresAt) { .sign(algorithm); } + public Long verifyAccessTokenAndDecodeMemberId(String accessToken) { + verifyAccessToken(accessToken); + return decodeMemberId(accessToken); + } + private void verifyToken(String token, String subject) { JWT.require(algorithm) .withIssuer(issuer) @@ -65,5 +70,4 @@ private void verifyToken(String token, String subject) { .build() .verify(token); } - } diff --git a/BE/src/main/java/louie/hanse/issuetracker/oauth/GithubAccessToken.java b/BE/src/main/java/louie/hanse/issuetracker/login/oauth/GithubAccessToken.java similarity index 91% rename from BE/src/main/java/louie/hanse/issuetracker/oauth/GithubAccessToken.java rename to BE/src/main/java/louie/hanse/issuetracker/login/oauth/GithubAccessToken.java index 62ed01e8b1..f4c5b564e1 100644 --- a/BE/src/main/java/louie/hanse/issuetracker/oauth/GithubAccessToken.java +++ b/BE/src/main/java/louie/hanse/issuetracker/login/oauth/GithubAccessToken.java @@ -1,4 +1,4 @@ -package louie.hanse.issuetracker.oauth; +package louie.hanse.issuetracker.login.oauth; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; diff --git a/BE/src/main/java/louie/hanse/issuetracker/oauth/GithubAccessTokenRequest.java b/BE/src/main/java/louie/hanse/issuetracker/login/oauth/GithubAccessTokenRequest.java similarity index 90% rename from BE/src/main/java/louie/hanse/issuetracker/oauth/GithubAccessTokenRequest.java rename to BE/src/main/java/louie/hanse/issuetracker/login/oauth/GithubAccessTokenRequest.java index 39f028a9c7..d344ae2932 100644 --- a/BE/src/main/java/louie/hanse/issuetracker/oauth/GithubAccessTokenRequest.java +++ b/BE/src/main/java/louie/hanse/issuetracker/login/oauth/GithubAccessTokenRequest.java @@ -1,4 +1,4 @@ -package louie.hanse.issuetracker.oauth; +package louie.hanse.issuetracker.login.oauth; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; diff --git a/BE/src/main/java/louie/hanse/issuetracker/oauth/GithubUser.java b/BE/src/main/java/louie/hanse/issuetracker/login/oauth/GithubUser.java similarity index 87% rename from BE/src/main/java/louie/hanse/issuetracker/oauth/GithubUser.java rename to BE/src/main/java/louie/hanse/issuetracker/login/oauth/GithubUser.java index 2399c091ee..aa159b6a5c 100644 --- a/BE/src/main/java/louie/hanse/issuetracker/oauth/GithubUser.java +++ b/BE/src/main/java/louie/hanse/issuetracker/login/oauth/GithubUser.java @@ -1,4 +1,4 @@ -package louie.hanse.issuetracker.oauth; +package louie.hanse.issuetracker.login.oauth; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; diff --git a/BE/src/main/java/louie/hanse/issuetracker/oauth/OAuthProperties.java b/BE/src/main/java/louie/hanse/issuetracker/login/oauth/OAuthProperties.java similarity index 92% rename from BE/src/main/java/louie/hanse/issuetracker/oauth/OAuthProperties.java rename to BE/src/main/java/louie/hanse/issuetracker/login/oauth/OAuthProperties.java index 20d53e94f5..f784e356eb 100644 --- a/BE/src/main/java/louie/hanse/issuetracker/oauth/OAuthProperties.java +++ b/BE/src/main/java/louie/hanse/issuetracker/login/oauth/OAuthProperties.java @@ -1,4 +1,4 @@ -package louie.hanse.issuetracker.oauth; +package louie.hanse.issuetracker.login.oauth; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/BE/src/main/java/louie/hanse/issuetracker/repository/CommentRepository.java b/BE/src/main/java/louie/hanse/issuetracker/repository/CommentRepository.java new file mode 100644 index 0000000000..44ff8b364b --- /dev/null +++ b/BE/src/main/java/louie/hanse/issuetracker/repository/CommentRepository.java @@ -0,0 +1,8 @@ +package louie.hanse.issuetracker.repository; + +import louie.hanse.issuetracker.domain.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { + +} diff --git a/BE/src/main/java/louie/hanse/issuetracker/repository/CustomIssueRepository.java b/BE/src/main/java/louie/hanse/issuetracker/repository/CustomIssueRepository.java new file mode 100644 index 0000000000..ea67ee2256 --- /dev/null +++ b/BE/src/main/java/louie/hanse/issuetracker/repository/CustomIssueRepository.java @@ -0,0 +1,11 @@ +package louie.hanse.issuetracker.repository; + +import louie.hanse.issuetracker.domain.Issue; +import louie.hanse.issuetracker.web.dto.issue.IssueSearchRequest; + +import java.util.List; + +public interface CustomIssueRepository { + List search(IssueSearchRequest issueSearchRequest, Long userId); + long searchReverseStatusCount(IssueSearchRequest issueSearchRequest, Long userId); +} diff --git a/BE/src/main/java/louie/hanse/issuetracker/repository/CustomIssueRepositoryImpl.java b/BE/src/main/java/louie/hanse/issuetracker/repository/CustomIssueRepositoryImpl.java new file mode 100644 index 0000000000..3c4cf5c97e --- /dev/null +++ b/BE/src/main/java/louie/hanse/issuetracker/repository/CustomIssueRepositoryImpl.java @@ -0,0 +1,77 @@ +package louie.hanse.issuetracker.repository; + +import com.querydsl.core.types.Predicate; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import louie.hanse.issuetracker.domain.*; +import louie.hanse.issuetracker.web.dto.issue.IssueSearchRequest; + +import java.util.List; + +import static louie.hanse.issuetracker.domain.QComment.*; +import static louie.hanse.issuetracker.domain.QIssue.issue; +import static louie.hanse.issuetracker.domain.QIssueLabel.issueLabel; +import static louie.hanse.issuetracker.domain.QIssueManager.*; +import static louie.hanse.issuetracker.domain.QLabel.label; + +@RequiredArgsConstructor +public class CustomIssueRepositoryImpl implements CustomIssueRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List search(IssueSearchRequest request, Long userId) { + return jpaQueryFactory.selectFrom(issue) + .distinct() + .leftJoin(issue.issueManagers, issueManager) + .leftJoin(issue.comments, comment) + .leftJoin(issue.issueLabels, issueLabel) + .leftJoin(issueLabel.label, label) + .where( + issue.status.eq(request.getStatus()), + issueWriterIdEq(request.getWriterId()), + issueManagerIdEq(request.getManagerId()), + commentWriterIdEq(userId), + labelIdEq(request.getLabelId()), + issueMilestoneIdEq(request.getMilestoneId()) + ).fetch(); + } + + @Override + public long searchReverseStatusCount(IssueSearchRequest request, Long userId) { + return jpaQueryFactory.selectFrom(issue) + .distinct() + .leftJoin(issue.issueManagers, issueManager) + .leftJoin(issue.comments, comment) + .leftJoin(issue.issueLabels, issueLabel) + .leftJoin(issueLabel.label, label) + .where( + issue.status.eq(request.getStatus().reverse()), + issueWriterIdEq(request.getWriterId()), + issueManagerIdEq(request.getManagerId()), + commentWriterIdEq(userId), + labelIdEq(request.getLabelId()), + issueMilestoneIdEq(request.getMilestoneId()) + ).fetch().size(); + } + + private Predicate issueMilestoneIdEq(Long id) { + return id == null ? null : issue.milestone.id.eq(id); + } + + private Predicate labelIdEq(Long id) { + return id == null ? null : label.id.eq(id); + } + + private Predicate commentWriterIdEq(Long id) { + return id == null ? null : comment.writer.id.eq(id); + } + + private Predicate issueManagerIdEq(Long id) { + return id == null ? null : issueManager.manager.id.eq(id); + } + + private Predicate issueWriterIdEq(Long id) { + return id == null ? null : issue.writer.id.eq(id); + } +} diff --git a/BE/src/main/java/louie/hanse/issuetracker/repository/IssueLabelRepository.java b/BE/src/main/java/louie/hanse/issuetracker/repository/IssueLabelRepository.java new file mode 100644 index 0000000000..d1b5f483cd --- /dev/null +++ b/BE/src/main/java/louie/hanse/issuetracker/repository/IssueLabelRepository.java @@ -0,0 +1,7 @@ +package louie.hanse.issuetracker.repository; + +import louie.hanse.issuetracker.domain.IssueLabel; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface IssueLabelRepository extends JpaRepository { +} diff --git a/BE/src/main/java/louie/hanse/issuetracker/repository/IssueManagerRepository.java b/BE/src/main/java/louie/hanse/issuetracker/repository/IssueManagerRepository.java new file mode 100644 index 0000000000..091fa8f730 --- /dev/null +++ b/BE/src/main/java/louie/hanse/issuetracker/repository/IssueManagerRepository.java @@ -0,0 +1,7 @@ +package louie.hanse.issuetracker.repository; + +import louie.hanse.issuetracker.domain.IssueManager; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface IssueManagerRepository extends JpaRepository { +} diff --git a/BE/src/main/java/louie/hanse/issuetracker/repository/IssueRepository.java b/BE/src/main/java/louie/hanse/issuetracker/repository/IssueRepository.java new file mode 100644 index 0000000000..2c6e1f41ea --- /dev/null +++ b/BE/src/main/java/louie/hanse/issuetracker/repository/IssueRepository.java @@ -0,0 +1,7 @@ +package louie.hanse.issuetracker.repository; + +import louie.hanse.issuetracker.domain.Issue; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface IssueRepository extends JpaRepository, CustomIssueRepository { +} diff --git a/BE/src/main/java/louie/hanse/issuetracker/repository/LabelRepository.java b/BE/src/main/java/louie/hanse/issuetracker/repository/LabelRepository.java new file mode 100644 index 0000000000..94744ecbb1 --- /dev/null +++ b/BE/src/main/java/louie/hanse/issuetracker/repository/LabelRepository.java @@ -0,0 +1,7 @@ +package louie.hanse.issuetracker.repository; + +import louie.hanse.issuetracker.domain.Label; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LabelRepository extends JpaRepository { +} diff --git a/BE/src/main/java/louie/hanse/issuetracker/repository/MilestoneRepository.java b/BE/src/main/java/louie/hanse/issuetracker/repository/MilestoneRepository.java new file mode 100644 index 0000000000..5c5a383df7 --- /dev/null +++ b/BE/src/main/java/louie/hanse/issuetracker/repository/MilestoneRepository.java @@ -0,0 +1,18 @@ +package louie.hanse.issuetracker.repository; + +import louie.hanse.issuetracker.domain.Milestone; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDate; +import java.util.List; + +public interface MilestoneRepository extends JpaRepository { + + @Query("select m from Milestone as m where m.completedDate < :currentDate") + List findClosedMilestone(@Param("currentDate") LocalDate currentDate); + + @Query("select m from Milestone as m where m.completedDate > :currentDate") + List findOpenedMilestone(@Param("currentDate") LocalDate currentDate); +} diff --git a/BE/src/main/java/louie/hanse/issuetracker/service/CommentService.java b/BE/src/main/java/louie/hanse/issuetracker/service/CommentService.java new file mode 100644 index 0000000000..a4bcbd72cf --- /dev/null +++ b/BE/src/main/java/louie/hanse/issuetracker/service/CommentService.java @@ -0,0 +1,35 @@ +package louie.hanse.issuetracker.service; + +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import louie.hanse.issuetracker.domain.Comment; +import louie.hanse.issuetracker.domain.Issue; +import louie.hanse.issuetracker.repository.CommentRepository; +import louie.hanse.issuetracker.web.dto.comment.CommentSaveResponse; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class CommentService { + + private final CommentRepository commentRepository; + private final IssueService issueService; + + @Transactional + public CommentSaveResponse write(Long issueId, String contents) { + Issue issue = issueService.findByIdOrThrow(issueId); + Comment comment = new Comment(issue, contents); + commentRepository.save(comment); + return new CommentSaveResponse(comment); + } + + @Transactional + public LocalDateTime edit(Long id, String contents) { + Comment comment = commentRepository.findById(id) + .orElseThrow(IllegalStateException::new); + comment.updateContents(contents); + return comment.getUpdatedDateTime(); + } +} diff --git a/BE/src/main/java/louie/hanse/issuetracker/service/IssueService.java b/BE/src/main/java/louie/hanse/issuetracker/service/IssueService.java new file mode 100644 index 0000000000..6505a15dc5 --- /dev/null +++ b/BE/src/main/java/louie/hanse/issuetracker/service/IssueService.java @@ -0,0 +1,90 @@ +package louie.hanse.issuetracker.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import louie.hanse.issuetracker.domain.*; +import louie.hanse.issuetracker.repository.*; +import louie.hanse.issuetracker.web.dto.issue.IssueDetailResponse; +import louie.hanse.issuetracker.web.dto.issue.IssueSaveRequest; +import louie.hanse.issuetracker.web.dto.issue.IssueSearchRequest; +import louie.hanse.issuetracker.web.dto.issue.IssueSearchResponse; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class IssueService { + private final IssueRepository issueRepository; + private final MemberRepository memberRepository; + private final LabelRepository labelRepository; + private final MilestoneRepository milestoneRepository; + + @Transactional + public void register(IssueSaveRequest issueSaveRequest, Long memberId) { + Member writer = memberRepository.findById(memberId) + .orElseThrow(IllegalStateException::new); + Issue issue = new Issue(issueSaveRequest.getTitle(), writer); + if (issueSaveRequest.getContents() != null) { + Comment comment = new Comment(issue, issueSaveRequest.getContents()); + } + if (issueSaveRequest.getMilestoneId() != null) { + Milestone milestone = milestoneRepository.findById(issueSaveRequest.getMilestoneId()) + .orElseThrow(IllegalStateException::new); + issue.updateMilestone(milestone); + } + + List managers = memberRepository.findAllById(issueSaveRequest.getManagerIds()); + for (Member manager : managers) { + IssueManager issueManager = new IssueManager(issue, manager); + } + List