diff --git a/.github/ISSUE_TEMPLATE/bug-report-template.md b/.github/ISSUE_TEMPLATE/bug-report-template.md new file mode 100644 index 00000000..c8286e51 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report-template.md @@ -0,0 +1,22 @@ +--- +name: Bug Report Template +about: 버그 리포트 이슈 템플릿 +title: "[BUG/#이슈번호] 이슈 내용" +labels: '' +assignees: '' + +--- + +## 어떤 버그인가요? + +> 어떤 버그인지 간결하게 설명해주세요 + +## 어떤 상황에서 발생한 버그인가요? + +> (가능하면) Given-When-Then 형식으로 서술해주세요 + +## 예상 결과 + +> 예상했던 정상적인 결과가 어떤 것이었는지 설명해주세요 + +## 참고할만한 자료(선택) diff --git a/.github/ISSUE_TEMPLATE/feature-template.md b/.github/ISSUE_TEMPLATE/feature-template.md new file mode 100644 index 00000000..f8088862 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-template.md @@ -0,0 +1,20 @@ +--- +name: Feature Template +about: 기능 추가 이슈 템플릿 +title: "[타입/#이슈번호] 이슈 내용" +labels: '' +assignees: '' + +--- + +## 어떤 기능인가요? + +> 추가하려는 기능에 대해 간결하게 설명해주세요 + +## 작업 상세 내용 + +- [ ] TODO +- [ ] TODO +- [ ] TODO + +## 참고할만한 자료(선택) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..420e398c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +### PR 타입(하나 이상의 PR 타입을 선택해주세요) +-[] 기능 추가 +-[] 기능 삭제 +-[] 버그 수정 +-[] 의존성, 환경 변수, 빌드 관련 코드 업데이트 + +### 반영 브랜치 +ex) feat/login -> dev + +### 작업 내용 +ex) 로그인 시, 구글 소셜 로그인 기능을 추가했습니다. + +### 테스트 결과 +ex) 베이스 브랜치에 포함되기 위한 코드는 모두 정상적으로 동작해야 합니다. 결과물에 대한 스크린샷, GIF, 혹은 라이브 diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 00000000..8901d07a --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,67 @@ +name: Java CI with Gradle + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build-docker-image: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Spring Boot 어플리케이션 Build + - name: Build with Gradle Wrapper + run: ./gradlew build + + # Docker 이미지 Build + - name: docker image build + run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/spring-cicd-test . + + # DockerHub Login (push 하기 위해) + - name: docker login + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + # Docker hub 로 push + - name: Docker Hub push + run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/spring-cicd-test + + # 위 과정에서 푸시한 이미지를 ec2에서 풀받아서 실행 + run-docker-image-on-ec2: + needs: build-docker-image + runs-on: self-hosted + + steps: + - name: docker pull + run : sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/spring-cicd-test + + - name: docker stop container + run: | + if [ $(sudo docker ps -a -q -f name=spring-cicd-test) ]; then + sudo docker stop spring-cicd-test + fi + + - name: docker run new container + run: sudo docker run --rm -it -d -p 80:8080 --name spring-cicd-test ${{ secrets.DOCKERHUB_USERNAME }}/spring-cicd-test + + - name: delete old docker image + run: sudo docker system prune -f diff --git a/.gitignore b/.gitignore index c2065bc2..8f62fc45 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ out/ ### VS Code ### .vscode/ + +.env +application-secrets.yml.DS_Store +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..47ba90bb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +#FR open jdk 11 버전의 환경을 구성 + FROM eclipse-temurin:17 + + # build가 되는 시점에 JAR_FILE이라는 변수 명에 build/libs/*.jar 선언 + # build/libs - gradle로 빌드했을 "때 jar 파일이 생성되는 경로 + ARG JAR_FILE=build/libs/*.jar + + # JAR_FILE을 app.jar로 복사 + COPY ${JAR_FILE} app.jar + + ENTRYPOINT ["java", "-jar", "/app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index fed0aa2c..9a9dc411 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,112 @@ # STACKPOT-BE -STACKPOT-BE + +## Commit Convention +- **[FEAT]** : 새로운 기능 구현 +- **[MOD]** : 코드 수정 및 내부 파일 수정 +- **[ADD]** : 부수적인 코드 추가 및 라이브러리 추가, 새로운 파일 생성 +- **[CHORE]** : 버전 코드 수정, 패키지 구조 변경, 타입 및 변수명 변경 등의 작은 작업 +- **[DEL]** : 쓸모없는 코드나 파일 삭제 +- **[UI]** : UI 작업 +- **[FIX]** : 버그 및 오류 해결 +- **[HOTFIX]** : issue나 QA에서 문의된 급한 버그 및 오류 해결 +- **[MERGE]** : 다른 브랜치와의 MERGE +- **[MOVE]** : 프로젝트 내 파일이나 코드의 이동 +- **[RENAME]** : 파일 이름 변경 +- **[REFACTOR]** : 전면 수정 +- **[DOCS]** : README나 WIKI 등의 문서 개정 + +- --- + +**📌 형식**: + +- `[커밋 타입/#이슈번호] 커밋 내용` + +**📌 예시** + +- `[feat/#32] User 도메인 구현` +- `[feat/#32] User 필드값 annotation 추가` + +## Branch Convention +1. **이슈 파기** + + **📌 형식** + + `[타입/#이슈번호] 이슈 내용` + + **📌 예시** + + - `[Feat/#11] User 도메인 구현` + - `[Refactor/#2] User 관련 DTO 수정` +2. **브랜치 파기** + + **📌 형식** + + - `유형/#이슈번호-what` + + **📌 예시** + + - `feat/#11-login-view-ui` +1. **PR 올리기** + + **📌 형식** + + - `[유형] where / what` + + **📌 예시** + + - `[FEAT] 로그인 뷰 / UI 구현` + + **📌 PR Convention** + + ``` + ### PR 타입(하나 이상의 PR 타입을 선택해주세요) + -[] 기능 추가 + -[] 기능 삭제 + -[] 버그 수정 + -[] 의존성, 환경 변수, 빌드 관련 코드 업데이트 + + ### 반영 브랜치 + ex) feat/login -> dev + + ### 변경 사항 + ex) 로그인 시, 구글 소셜 로그인 기능을 추가했습니다. + + ### 테스트 결과 + ex) 베이스 브랜치에 포함되기 위한 코드는 모두 정상적으로 동작해야 합니다. 결과물에 대한 스크린샷, GIF, 혹은 라이브 + ``` +## PR Convention +**📌 형식** + +- `[유형/#이슈번호] where / what` + +**📌 예시** + +- `[FEAT/#3] 로그인 뷰 / UI 구현` + +**📌 PR 프로세스** + +1. **PR 생성**: 작업을 완료한 후, 변경 사항을 설명하는 PR을 생성합니다. +2. **코드 리뷰 요청**: PR이 생성되면 팀원들에게 코드 리뷰를 요청합니다. +3. **코드 리뷰 진행**: 리뷰어는 코드를 검토하고 피드백을 제공합니다. +4. **피드백 대응**: PR 작성자는 리뷰어의 피드백을 반영하여 코드를 수정합니다. +5. **리뷰어 동의**: 리뷰어는 수정된 코드를 다시 검토하고 동의합니다. +6. **PR 병합**: 필요한 승인 수가 충족되면, PR을 메인 브랜치에 병합합니다. + +**📌 PR 템플릿** + +``` +### PR 타입(하나 이상의 PR 타입을 선택해주세요) +-[] 기능 추가 +-[] 기능 삭제 +-[] 버그 수정 +-[] 의존성, 환경 변수, 빌드 관련 코드 업데이트 + +### 반영 브랜치 +ex) feat/login -> dev + +### 작업 내용 +ex) 로그인 시, 구글 소셜 로그인 기능을 추가했습니다. + +### 테스트 결과 +ex) 베이스 브랜치에 포함되기 위한 코드는 모두 정상적으로 동작해야 합니다. 결과물에 대한 스크린샷, GIF, 혹은 라이브 +``` diff --git a/build.gradle b/build.gradle index ea0071c4..09d933e0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.4.1' + id 'org.springframework.boot' version '3.3.6' id 'io.spring.dependency-management' version '1.1.7' } @@ -24,15 +24,63 @@ repositories { } dependencies { + // Spring Boot Core implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + // Lombok compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + + // Database + runtimeOnly 'com.mysql:mysql-connector-j' + runtimeOnly 'mysql:mysql-connector-java:8.0.33' + + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // Thymeleaf Security Integration + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE' + + // Swagger/OpenAPI + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' + + // AWS Integration + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // Test Dependencies testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'mysql:mysql-connector-java:8.0.33' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.security:spring-security-crypto' + + + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'io.netty:netty-resolver-dns-native-macos:4.1.95.Final:osx-aarch_64' + + + //Email + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' + implementation 'mysql:mysql-connector-java:8.0.33' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.security:spring-security-crypto' + //dotenv + implementation 'io.github.cdimascio:java-dotenv:5.2.2' + + } + tasks.named('test') { useJUnitPlatform() } diff --git a/src/main/java/stackpot/stackpot/StackpotApplication.java b/src/main/java/stackpot/stackpot/StackpotApplication.java index bd79a86e..ca431423 100644 --- a/src/main/java/stackpot/stackpot/StackpotApplication.java +++ b/src/main/java/stackpot/stackpot/StackpotApplication.java @@ -2,12 +2,16 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling +@EnableJpaAuditing public class StackpotApplication { public static void main(String[] args) { - SpringApplication.run(StackpotApplication.class, args); + SpringApplication.run(StackpotApplication.class, args);; } } diff --git a/src/main/java/stackpot/stackpot/Validation/annotation/ValidRole.java b/src/main/java/stackpot/stackpot/Validation/annotation/ValidRole.java new file mode 100644 index 00000000..90789bbb --- /dev/null +++ b/src/main/java/stackpot/stackpot/Validation/annotation/ValidRole.java @@ -0,0 +1,17 @@ +package stackpot.stackpot.Validation.annotation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import stackpot.stackpot.Validation.validator.RoleValidator; + +import java.lang.annotation.*; + +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = RoleValidator.class) +@Documented +public @interface ValidRole { + String message() default "유효하지 않은 모집 역할입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/stackpot/stackpot/Validation/validator/RoleValidator.java b/src/main/java/stackpot/stackpot/Validation/validator/RoleValidator.java new file mode 100644 index 00000000..59d59f10 --- /dev/null +++ b/src/main/java/stackpot/stackpot/Validation/validator/RoleValidator.java @@ -0,0 +1,22 @@ +package stackpot.stackpot.Validation.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import stackpot.stackpot.Validation.annotation.ValidRole; +import stackpot.stackpot.domain.enums.Role; + +public class RoleValidator implements ConstraintValidator { + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null || value.isEmpty()) { + return false; + } + try { + Role.valueOf(value.toUpperCase()); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } +} diff --git a/src/main/java/stackpot/stackpot/apiPayload/ApiResponse.java b/src/main/java/stackpot/stackpot/apiPayload/ApiResponse.java new file mode 100644 index 00000000..e0627a2a --- /dev/null +++ b/src/main/java/stackpot/stackpot/apiPayload/ApiResponse.java @@ -0,0 +1,35 @@ +package stackpot.stackpot.apiPayload; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.AllArgsConstructor; +import lombok.Getter; +import stackpot.stackpot.apiPayload.code.BaseCode; +import stackpot.stackpot.apiPayload.code.status.SuccessStatus; + +@Getter +@AllArgsConstructor +@JsonPropertyOrder({"isSuccess", "code", " message", "result"}) +public class ApiResponse { + + @JsonProperty("isSuccess") + private final Boolean isSuccsee; + private final String code; + private final String message; + @JsonInclude(JsonInclude.Include.NON_NULL) + private T result; + + public static ApiResponse onSuccess(T result){ + return new ApiResponse<>(true, SuccessStatus._OK.getCode() , SuccessStatus._OK.getMessage(), result); + } + + public static ApiResponse of(BaseCode code, T result){ + return new ApiResponse<>(true, code.getReasonHttpStatus().getCode() , code.getReasonHttpStatus().getMessage(), result); + } + + public static ApiResponse onFailure(String code, String message, T data){ + return new ApiResponse<>(false, code, message, data); + } + +} diff --git a/src/main/java/stackpot/stackpot/apiPayload/code/BaseCode.java b/src/main/java/stackpot/stackpot/apiPayload/code/BaseCode.java new file mode 100644 index 00000000..8c47b59f --- /dev/null +++ b/src/main/java/stackpot/stackpot/apiPayload/code/BaseCode.java @@ -0,0 +1,7 @@ +package stackpot.stackpot.apiPayload.code; + +public interface BaseCode { + + ReasonDTO getReason(); + ReasonDTO getReasonHttpStatus(); +} diff --git a/src/main/java/stackpot/stackpot/apiPayload/code/BaseErrorCode.java b/src/main/java/stackpot/stackpot/apiPayload/code/BaseErrorCode.java new file mode 100644 index 00000000..53838aea --- /dev/null +++ b/src/main/java/stackpot/stackpot/apiPayload/code/BaseErrorCode.java @@ -0,0 +1,6 @@ +package stackpot.stackpot.apiPayload.code; + +public interface BaseErrorCode { + ErrorReasonDTO getReason(); + ErrorReasonDTO getReasonHttpStatus(); +} diff --git a/src/main/java/stackpot/stackpot/apiPayload/code/ErrorReasonDTO.java b/src/main/java/stackpot/stackpot/apiPayload/code/ErrorReasonDTO.java new file mode 100644 index 00000000..d544cc93 --- /dev/null +++ b/src/main/java/stackpot/stackpot/apiPayload/code/ErrorReasonDTO.java @@ -0,0 +1,17 @@ +package stackpot.stackpot.apiPayload.code; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@Builder +public class ErrorReasonDTO { + private HttpStatus httpStatus; + + private final boolean isSuccess; + private final String code; + private final String message; + + public boolean getIsSuccess(){return isSuccess;} +} diff --git a/src/main/java/stackpot/stackpot/apiPayload/code/ReasonDTO.java b/src/main/java/stackpot/stackpot/apiPayload/code/ReasonDTO.java new file mode 100644 index 00000000..c27d9254 --- /dev/null +++ b/src/main/java/stackpot/stackpot/apiPayload/code/ReasonDTO.java @@ -0,0 +1,16 @@ +package stackpot.stackpot.apiPayload.code; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; +@Getter +@Builder +public class ReasonDTO { + private HttpStatus httpStatus; + + private final boolean isSuccess; + private final String code; + private final String message; + + public boolean getIsSuccess(){return isSuccess;} +} diff --git a/src/main/java/stackpot/stackpot/apiPayload/code/status/ErrorStatus.java b/src/main/java/stackpot/stackpot/apiPayload/code/status/ErrorStatus.java new file mode 100644 index 00000000..a25af02d --- /dev/null +++ b/src/main/java/stackpot/stackpot/apiPayload/code/status/ErrorStatus.java @@ -0,0 +1,66 @@ +package stackpot.stackpot.apiPayload.code.status; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import stackpot.stackpot.apiPayload.code.BaseErrorCode; +import stackpot.stackpot.apiPayload.code.ErrorReasonDTO; + +@Getter +@AllArgsConstructor +public enum ErrorStatus implements BaseErrorCode { + // 가장 일반적인 응답 + _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."), + _BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."), + _UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."), + _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), + + MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4004", "등록된 사용자가 없습니다."), + + // Pot 관련 에러 + POT_NOT_FOUND(HttpStatus.NOT_FOUND, "POT4004", "팟이 존재하지 않습니다."), + POT_FORBIDDEN(HttpStatus.FORBIDDEN, "POT4003", "팟 생성자가 아닙니다."), + + // 모집 관련 에러 + RECRUITMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "RECRUITMENT4004", "모집 내역이 없습니다."), + + // 지원 관련 에러 + APPLICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "APPLICATION4004", "지원 내역이 없습니다."), + + // 페이지 관련 에러 + INVALID_PAGE(HttpStatus.BAD_REQUEST, "PAGE4000", "Page는 1이상입니다."), + + // 투두 관련 에러 코드 + USER_TODO_NOT_FOUND(HttpStatus.BAD_REQUEST,"TODO4004", "해당 Pot ID 및 Todo ID에 대한 투두를 찾을 수 없습니다."), + USER_TODO_UNAUTHORIZED(HttpStatus.FORBIDDEN,"TODO4003", "해당 투두에 대한 수정 권한이 없습니다."), + + // Enum 관련 에러 + INVALID_POT_STATUS(HttpStatus.BAD_REQUEST, "POT_STATUS4000", "Pot Status 형식이 올바르지 않습니다 (RECRUITING / ONGOING / COMPLETED)"), + INVALID_POT_MODE_OF_OPERATION(HttpStatus.BAD_REQUEST, "MODE_OF_OPERATION4000", "Pot ModeOfOperation 형식이 올바르지 않습니다 (ONLINE / OFFLINE / HYBRID)"), + INVALID_ROLE(HttpStatus.BAD_REQUEST, "ROLE4000", "Role 형식이 올바르지 않습니다 (FRONTEND / DESIGN / BACKEND / PLANNING)"); + + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ErrorReasonDTO getReason() { + return ErrorReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(false) + .build(); + } + + @Override + public ErrorReasonDTO getReasonHttpStatus() { + return ErrorReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(false) + .httpStatus(httpStatus) + .build() + ; + } +} diff --git a/src/main/java/stackpot/stackpot/apiPayload/code/status/SuccessStatus.java b/src/main/java/stackpot/stackpot/apiPayload/code/status/SuccessStatus.java new file mode 100644 index 00000000..92f85501 --- /dev/null +++ b/src/main/java/stackpot/stackpot/apiPayload/code/status/SuccessStatus.java @@ -0,0 +1,37 @@ +package stackpot.stackpot.apiPayload.code.status; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import stackpot.stackpot.apiPayload.code.BaseCode; +import stackpot.stackpot.apiPayload.code.ReasonDTO; + +@Getter +@AllArgsConstructor +public enum SuccessStatus implements BaseCode { + + _OK(HttpStatus.OK, "COMMON200", "성공입니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDTO getReason() { + return ReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(true) + .build(); + } + + @Override + public ReasonDTO getReasonHttpStatus() { + return ReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(true) + .httpStatus(httpStatus) + .build(); + } +} diff --git a/src/main/java/stackpot/stackpot/apiPayload/exception/ExceptionAdvice.java b/src/main/java/stackpot/stackpot/apiPayload/exception/ExceptionAdvice.java new file mode 100644 index 00000000..c017c6e4 --- /dev/null +++ b/src/main/java/stackpot/stackpot/apiPayload/exception/ExceptionAdvice.java @@ -0,0 +1,126 @@ +package stackpot.stackpot.apiPayload.exception; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import stackpot.stackpot.apiPayload.ApiResponse; +import stackpot.stackpot.apiPayload.code.ErrorReasonDTO; +import stackpot.stackpot.apiPayload.code.status.ErrorStatus; +import stackpot.stackpot.web.controller.*; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + + +@Slf4j +@RestControllerAdvice(annotations = {RestController.class}, basePackageClasses = {UserController.class, PotController.class, FeedController.class, MyPotController.class, PotApplicationController.class, PotMemberController.class}) +public class ExceptionAdvice extends ResponseEntityExceptionHandler { + + + @ExceptionHandler + public ResponseEntity validation(ConstraintViolationException e, WebRequest request) { + String errorMessage = e.getConstraintViolations().stream() + .map(constraintViolation -> constraintViolation.getMessage()) + .findFirst() + .orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생")); + + return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY,request); + } + + @Override + public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + + Map errors = new LinkedHashMap<>(); + + e.getBindingResult().getFieldErrors().stream() + .forEach(fieldError -> { + String fieldName = fieldError.getField(); + String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse(""); + errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage); + }); + + return handleExceptionInternalArgs(e,HttpHeaders.EMPTY,ErrorStatus.valueOf("_BAD_REQUEST"),request,errors); + } + + @ExceptionHandler + public ResponseEntity exception(Exception e, WebRequest request) { + e.printStackTrace(); + + return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(),request, e.getMessage()); + } + + @ExceptionHandler(value = GeneralException.class) + public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) { + ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus(); + return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e, WebRequest request) { + return handleExceptionInternalFalse(e, ErrorStatus.INVALID_ROLE, HttpHeaders.EMPTY, HttpStatus.BAD_REQUEST, request, e.getMessage()); + } + + private ResponseEntity handleExceptionInternal(Exception e, ErrorReasonDTO reason, + HttpHeaders headers, HttpServletRequest request) { + + ApiResponse body = ApiResponse.onFailure(reason.getCode(),reason.getMessage(),null); +// e.printStackTrace(); + + WebRequest webRequest = new ServletWebRequest(request); + return super.handleExceptionInternal( + e, + body, + headers, + reason.getHttpStatus(), + webRequest + ); + } + + private ResponseEntity handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus, + HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) { + ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorPoint); + return super.handleExceptionInternal( + e, + body, + headers, + status, + request + ); + } + + private ResponseEntity handleExceptionInternalArgs(Exception e, HttpHeaders headers, ErrorStatus errorCommonStatus, + WebRequest request, Map errorArgs) { + ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorArgs); + return super.handleExceptionInternal( + e, + body, + headers, + errorCommonStatus.getHttpStatus(), + request + ); + } + + private ResponseEntity handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus, + HttpHeaders headers, WebRequest request) { + ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null); + return super.handleExceptionInternal( + e, + body, + headers, + errorCommonStatus.getHttpStatus(), + request + ); + } +} diff --git a/src/main/java/stackpot/stackpot/apiPayload/exception/GeneralException.java b/src/main/java/stackpot/stackpot/apiPayload/exception/GeneralException.java new file mode 100644 index 00000000..c08440c4 --- /dev/null +++ b/src/main/java/stackpot/stackpot/apiPayload/exception/GeneralException.java @@ -0,0 +1,20 @@ +package stackpot.stackpot.apiPayload.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import stackpot.stackpot.apiPayload.code.BaseErrorCode; +import stackpot.stackpot.apiPayload.code.ErrorReasonDTO; + +@Getter +@AllArgsConstructor +public class GeneralException extends RuntimeException{ + + private BaseErrorCode code; + + public ErrorReasonDTO getErrorReason(){ + return this.code.getReason(); + } + public ErrorReasonDTO getErrorReasonHttpStatus(){ + return this.code.getReasonHttpStatus(); + } +} diff --git a/src/main/java/stackpot/stackpot/apiPayload/exception/handler/ApplicationHandler.java b/src/main/java/stackpot/stackpot/apiPayload/exception/handler/ApplicationHandler.java new file mode 100644 index 00000000..3d908501 --- /dev/null +++ b/src/main/java/stackpot/stackpot/apiPayload/exception/handler/ApplicationHandler.java @@ -0,0 +1,10 @@ +package stackpot.stackpot.apiPayload.exception.handler; + +import stackpot.stackpot.apiPayload.code.BaseErrorCode; +import stackpot.stackpot.apiPayload.exception.GeneralException; + +public class ApplicationHandler extends GeneralException { + public ApplicationHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/stackpot/stackpot/apiPayload/exception/handler/EnumHandler.java b/src/main/java/stackpot/stackpot/apiPayload/exception/handler/EnumHandler.java new file mode 100644 index 00000000..509673be --- /dev/null +++ b/src/main/java/stackpot/stackpot/apiPayload/exception/handler/EnumHandler.java @@ -0,0 +1,10 @@ +package stackpot.stackpot.apiPayload.exception.handler; + +import stackpot.stackpot.apiPayload.code.BaseErrorCode; +import stackpot.stackpot.apiPayload.exception.GeneralException; + +public class EnumHandler extends GeneralException { + public EnumHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/stackpot/stackpot/apiPayload/exception/handler/MemberHandler.java b/src/main/java/stackpot/stackpot/apiPayload/exception/handler/MemberHandler.java new file mode 100644 index 00000000..86924cd9 --- /dev/null +++ b/src/main/java/stackpot/stackpot/apiPayload/exception/handler/MemberHandler.java @@ -0,0 +1,10 @@ +package stackpot.stackpot.apiPayload.exception.handler; + +import stackpot.stackpot.apiPayload.code.BaseErrorCode; +import stackpot.stackpot.apiPayload.exception.GeneralException; + +public class MemberHandler extends GeneralException { + public MemberHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/stackpot/stackpot/apiPayload/exception/handler/PotHandler.java b/src/main/java/stackpot/stackpot/apiPayload/exception/handler/PotHandler.java new file mode 100644 index 00000000..ead48947 --- /dev/null +++ b/src/main/java/stackpot/stackpot/apiPayload/exception/handler/PotHandler.java @@ -0,0 +1,10 @@ +package stackpot.stackpot.apiPayload.exception.handler; + +import stackpot.stackpot.apiPayload.code.BaseErrorCode; +import stackpot.stackpot.apiPayload.exception.GeneralException; + +public class PotHandler extends GeneralException { + public PotHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/stackpot/stackpot/apiPayload/exception/handler/RecruitmentHandler.java b/src/main/java/stackpot/stackpot/apiPayload/exception/handler/RecruitmentHandler.java new file mode 100644 index 00000000..d0b28ffe --- /dev/null +++ b/src/main/java/stackpot/stackpot/apiPayload/exception/handler/RecruitmentHandler.java @@ -0,0 +1,10 @@ +package stackpot.stackpot.apiPayload.exception.handler; + +import stackpot.stackpot.apiPayload.code.BaseErrorCode; +import stackpot.stackpot.apiPayload.exception.GeneralException; + +public class RecruitmentHandler extends GeneralException { + public RecruitmentHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/stackpot/stackpot/aws/s3/AmazonS3Manager.java b/src/main/java/stackpot/stackpot/aws/s3/AmazonS3Manager.java new file mode 100644 index 00000000..da2ecdc8 --- /dev/null +++ b/src/main/java/stackpot/stackpot/aws/s3/AmazonS3Manager.java @@ -0,0 +1,40 @@ +package stackpot.stackpot.aws.s3; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import stackpot.stackpot.config.AmazonConfig; +import stackpot.stackpot.domain.Uuid; + +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AmazonS3Manager{ + + private final AmazonS3 amazonS3; + + private final AmazonConfig amazonConfig; + + + public String uploadFile(String keyName, MultipartFile file){ + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(file.getSize()); + try { + amazonS3.putObject(new PutObjectRequest(amazonConfig.getBucket(), keyName, file.getInputStream(), metadata)); + } catch (IOException e){ + log.error("error at AmazonS3Manager uploadFile : {}", (Object) e.getStackTrace()); + } + + return amazonS3.getUrl(amazonConfig.getBucket(), keyName).toString(); + } + + public String generateFeedFileKeyName(Uuid uuid) { + return amazonConfig.getFeedFilePath() + '/' + uuid.getUuid(); + } +} diff --git a/src/main/java/stackpot/stackpot/aws/s3/UuidRepository.java b/src/main/java/stackpot/stackpot/aws/s3/UuidRepository.java new file mode 100644 index 00000000..0988d425 --- /dev/null +++ b/src/main/java/stackpot/stackpot/aws/s3/UuidRepository.java @@ -0,0 +1,4 @@ +package stackpot.stackpot.aws.s3; + +public class UuidRepository { +} diff --git a/src/main/java/stackpot/stackpot/config/AmazonConfig.java b/src/main/java/stackpot/stackpot/config/AmazonConfig.java new file mode 100644 index 00000000..07f9a26d --- /dev/null +++ b/src/main/java/stackpot/stackpot/config/AmazonConfig.java @@ -0,0 +1,54 @@ +package stackpot.stackpot.config; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Getter +public class AmazonConfig { + + + private AWSCredentials awsCredentials; + + @org.springframework.beans.factory.annotation.Value("${spring.cloud.aws.credentials.accessKey}") + private String accessKey; + + @org.springframework.beans.factory.annotation.Value("${spring.cloud.aws.credentials.secretKey}") + private String secretKey; + + @org.springframework.beans.factory.annotation.Value("${spring.cloud.aws.region.static}") + private String region; + + @org.springframework.beans.factory.annotation.Value("${spring.cloud.aws.s3.bucket}") + private String bucket; + + @org.springframework.beans.factory.annotation.Value("${spring.cloud.aws.s3.path.FeedFile}") + private String FeedFilePath; + + @PostConstruct + public void init() { + this.awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + } + + @Bean + public AmazonS3 amazonS3() { + AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } + + @Bean + public AWSCredentialsProvider awsCredentialsProvider() { + return new AWSStaticCredentialsProvider(awsCredentials); + } +} diff --git a/src/main/java/stackpot/stackpot/config/OpenAIConfig.java b/src/main/java/stackpot/stackpot/config/OpenAIConfig.java new file mode 100644 index 00000000..657bd029 --- /dev/null +++ b/src/main/java/stackpot/stackpot/config/OpenAIConfig.java @@ -0,0 +1,20 @@ +package stackpot.stackpot.config; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Getter +@Configuration +public class OpenAIConfig { + + @Value("${OPEN_API_KEY}") + private String apiKey; + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/config/SwaggerConfig.java b/src/main/java/stackpot/stackpot/config/SwaggerConfig.java new file mode 100644 index 00000000..2db0fab0 --- /dev/null +++ b/src/main/java/stackpot/stackpot/config/SwaggerConfig.java @@ -0,0 +1,44 @@ +package stackpot.stackpot.config; + +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI stackPotAPI() { + Info info = new Info() + .title("StackPot API") + .description("StackPotAPI API 명세서") + .version("1.0.0"); + + String jwtSchemeName = "JWT TOKEN"; + + // API 요청 헤더에 인증 정보 포함 + SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtSchemeName); + + // SecuritySchemes 등록 + Components components = new Components() + .addSecuritySchemes(jwtSchemeName, new SecurityScheme() + .name(jwtSchemeName) + .type(SecurityScheme.Type.HTTP) // HTTP 방식 + .scheme("bearer") + .bearerFormat("JWT")); + + return new OpenAPI() + .info(info) // API 정보 설정 + .addServersItem(new Server().url("http://localhost:8080").description("Dev server")) // 서버 URL 설정 + .addServersItem(new Server().url("https://api.stackpot.co.kr").description("Production server")) // 서버 URL 설정 + .addSecurityItem(securityRequirement) // SecurityRequirement 추가 + .components(components); // SecuritySchemes 등록 + } +} diff --git a/src/main/java/stackpot/stackpot/config/security/JwtAuthenticationFilter.java b/src/main/java/stackpot/stackpot/config/security/JwtAuthenticationFilter.java new file mode 100644 index 00000000..2c74e7d6 --- /dev/null +++ b/src/main/java/stackpot/stackpot/config/security/JwtAuthenticationFilter.java @@ -0,0 +1,57 @@ +package stackpot.stackpot.config.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.security.core.Authentication; + +import java.io.IOException; +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String token = resolveToken(request); + + try { + if (token != null) { + System.out.println("Token found: " + token); + if (jwtTokenProvider.validateToken(token)) { + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + System.out.println("Authentication set in SecurityContext: " + authentication.getName()); + } else { + System.out.println("Invalid or expired token."); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("Invalid or expired token."); + return; + } + } else { + System.out.println("No token found in the request."); + } + filterChain.doFilter(request, response); + } catch (Exception ex) { + System.out.println("Exception in JwtAuthenticationFilter: " + ex.getMessage()); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.getWriter().write("Internal server error occurred."); + } + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + System.out.println("Authorization header is missing or does not start with 'Bearer '."); + return null; + } +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/config/security/JwtTokenProvider.java b/src/main/java/stackpot/stackpot/config/security/JwtTokenProvider.java new file mode 100644 index 00000000..c5c7a2d7 --- /dev/null +++ b/src/main/java/stackpot/stackpot/config/security/JwtTokenProvider.java @@ -0,0 +1,73 @@ +package stackpot.stackpot.config.security; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.security.core.Authentication; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.web.dto.TokenServiceResponse; + +import java.security.Key; +import java.util.Date; + +@RequiredArgsConstructor +@Component +public class JwtTokenProvider { + + @Value("${jwt.secret}") + private String secretKey; + private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 60; //1시간 + private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24; // 1일 + private final UserDetailsService userDetailsService; + + // JWT 생성 (이메일 포함) + public TokenServiceResponse createToken(User user) { + Claims claims = Jwts.claims().setSubject(user.getEmail()); + + Date now = new Date(); + + String accessToken = Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + ACCESS_TOKEN_EXPIRE_TIME)) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + + String refreshToken = Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRE_TIME)) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + return TokenServiceResponse.of(accessToken, refreshToken); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + + public String getEmailFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + } + + public Authentication getAuthentication(String token) { + String email = getEmailFromToken(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(email); // 이메일로 사용자 로드 + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/config/security/SecurityConfig.java b/src/main/java/stackpot/stackpot/config/security/SecurityConfig.java new file mode 100644 index 00000000..44debb6b --- /dev/null +++ b/src/main/java/stackpot/stackpot/config/security/SecurityConfig.java @@ -0,0 +1,74 @@ +package stackpot.stackpot.config.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import stackpot.stackpot.web.dto.TokenServiceResponse; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import static org.springframework.security.config.Customizer.withDefaults; + +@EnableWebSecurity +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { // security를 적용하지 않을 리소스 + return web -> web.ignoring() + // error endpoint를 열어줘야 함, favicon.ico 추가! + .requestMatchers("/error", "/favicon.ico"); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtTokenProvider jwtTokenProvider) throws Exception { + http.formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .cors(withDefaults()) + .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) + .sessionManagement( + session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) +// .oauth2Login( +// oauth -> oauth.userInfoEndpoint(config -> config.userService(customOAuth2UserService)) +// .successHandler(successHandler(jwtTokenProvider))) + .authorizeHttpRequests(request -> request + .requestMatchers("/", "/home", "/swagger-ui/**", "/v3/api-docs/**", "/v3/api-docs/**", "users/oauth/kakao").permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.addAllowedOrigin("http://localhost:3000"); + configuration.addAllowedOrigin("http://localhost:8080"); + configuration.addAllowedOrigin("https://stackpot.co.kr"); + configuration.addAllowedOrigin("https://api.stackpot.co.kr"); + configuration.addAllowedMethod("*"); + configuration.addAllowedHeader("*"); + configuration.setAllowCredentials(true); // 인증 정보 포함 허용 + configuration.setMaxAge(3600L); // 캐싱 시간 (1시간) + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + +// @Bean +// public PasswordEncoder passwordEncoder() { +// return new BCryptPasswordEncoder(); +// } +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/config/security/WebConfig.java b/src/main/java/stackpot/stackpot/config/security/WebConfig.java new file mode 100644 index 00000000..73e64604 --- /dev/null +++ b/src/main/java/stackpot/stackpot/config/security/WebConfig.java @@ -0,0 +1,25 @@ +package stackpot.stackpot.config.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig { + + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") // 모든 엔드포인트에 대해 CORS 허용 + .allowedOrigins("http://localhost:3000", "http://localhost:8080","https://stackpot.co.kr","https://api.stackpot.co.kr") // 허용할 도메인 + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메서드 + .allowedHeaders("Authorization", "Content-Type", "X-Requested-With") // 허용할 헤더 + .allowCredentials(true) // 인증 정보 포함 (쿠키, 헤더 등) + .maxAge(3600); // CORS 요청의 캐싱 시간 (1시간) + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/converter/FeedConverter.java b/src/main/java/stackpot/stackpot/converter/FeedConverter.java new file mode 100644 index 00000000..68932b4c --- /dev/null +++ b/src/main/java/stackpot/stackpot/converter/FeedConverter.java @@ -0,0 +1,13 @@ +package stackpot.stackpot.converter; + + +import stackpot.stackpot.domain.Feed; +import stackpot.stackpot.web.dto.FeedRequestDto; +import stackpot.stackpot.web.dto.FeedResponseDto; +import stackpot.stackpot.web.dto.FeedSearchResponseDto; + +public interface FeedConverter { + FeedResponseDto.FeedDto feedDto(Feed feed, long popularity, long likeCount); + Feed toFeed(FeedRequestDto.createDto request); + FeedSearchResponseDto toSearchDto(Feed feed); +} diff --git a/src/main/java/stackpot/stackpot/converter/FeedConverterImpl.java b/src/main/java/stackpot/stackpot/converter/FeedConverterImpl.java new file mode 100644 index 00000000..d220b753 --- /dev/null +++ b/src/main/java/stackpot/stackpot/converter/FeedConverterImpl.java @@ -0,0 +1,77 @@ +package stackpot.stackpot.converter; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import stackpot.stackpot.domain.Feed; +import stackpot.stackpot.repository.FeedLikeRepository; +import stackpot.stackpot.web.dto.FeedRequestDto; +import stackpot.stackpot.web.dto.FeedResponseDto; +import stackpot.stackpot.web.dto.FeedSearchResponseDto; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +@RequiredArgsConstructor +@Component +public class FeedConverterImpl implements FeedConverter{ + + private final FeedLikeRepository feedLikeRepository; + + + @Override + public FeedResponseDto.FeedDto feedDto(Feed feed, long popularity, long likeCount) { + return FeedResponseDto.FeedDto.builder() + .id(feed.getFeedId()) + .writer(feed.getUser().getNickname()) + .category(feed.getCategory()) + .title(feed.getTitle()) + .content(feed.getContent()) + .popularity(popularity) + .likeCount(likeCount) + .createdAt(formatLocalDateTime(feed.getCreatedAt())) + .build(); + } + + @Override + public Feed toFeed(FeedRequestDto.createDto request) { + return Feed.builder() + .title(request.getTitle()) + .content(request.getContent()) + .category(request.getCategor()) + .visibility(request.getVisibility()) + .build(); + } + + // 날짜 포맷 적용 메서드 + private String formatLocalDateTime(LocalDateTime dateTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 M월 d일 H:mm"); + return (dateTime != null) ? dateTime.format(formatter) : "날짜 없음"; + } + + + public FeedSearchResponseDto toSearchDto(Feed feed) { + Long likeCount = feedLikeRepository.countByFeed(feed); // 좋아요 개수 조회 + // 역할 이름 매핑 (유효한 역할만 처리) + String roleName = feed.getUser().getRole() != null ? feed.getUser().getRole().name() : "멤버"; + String nicknameWithRole = feed.getUser().getNickname() + " " + mapRoleName(roleName) ; + + return FeedSearchResponseDto.builder() + .feedId(feed.getFeedId()) + .title(feed.getTitle()) + .content(feed.getContent()) + .creatorNickname(nicknameWithRole) // 닉네임과 역할 포함 + .creatorRole(roleName) // 역할 이름 + .createdAt(formatLocalDateTime(feed.getCreatedAt())) // 시간 포맷 적용 + .likeCount(likeCount) // 좋아요 개수 포함 + .build(); + } + + private String mapRoleName(String roleName) { + return switch (roleName) { + case "BACKEND" -> "양파"; + case "FRONTEND" -> "버섯"; + case "DESIGN" -> "브로콜리"; + case "PLANNING" -> "당근"; + default -> "멤버"; + }; + } +} diff --git a/src/main/java/stackpot/stackpot/converter/PotApplicationConverter/PotApplicationConverter.java b/src/main/java/stackpot/stackpot/converter/PotApplicationConverter/PotApplicationConverter.java new file mode 100644 index 00000000..6f055ebb --- /dev/null +++ b/src/main/java/stackpot/stackpot/converter/PotApplicationConverter/PotApplicationConverter.java @@ -0,0 +1,13 @@ +package stackpot.stackpot.converter.PotApplicationConverter; + +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.mapping.PotApplication; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.web.dto.PotApplicationRequestDto; +import stackpot.stackpot.web.dto.PotApplicationResponseDto; + +public interface PotApplicationConverter { + PotApplication toEntity(PotApplicationRequestDto dto, Pot pot, User user); + + PotApplicationResponseDto toDto(PotApplication entity); +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/converter/PotApplicationConverter/PotApplicationConverterImpl.java b/src/main/java/stackpot/stackpot/converter/PotApplicationConverter/PotApplicationConverterImpl.java new file mode 100644 index 00000000..dbdf1bf6 --- /dev/null +++ b/src/main/java/stackpot/stackpot/converter/PotApplicationConverter/PotApplicationConverterImpl.java @@ -0,0 +1,71 @@ +package stackpot.stackpot.converter.PotApplicationConverter; + +import org.springframework.stereotype.Component; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.enums.Role; +import stackpot.stackpot.domain.mapping.PotApplication; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.enums.ApplicationStatus; +import stackpot.stackpot.web.dto.PotApplicationRequestDto; +import stackpot.stackpot.web.dto.PotApplicationResponseDto; + +import java.time.LocalDateTime; +import java.util.Map; + +@Component +public class PotApplicationConverterImpl implements PotApplicationConverter { + + @Override + public PotApplication toEntity(PotApplicationRequestDto dto, Pot pot, User user) { + // User와 Pot 객체가 null인지 확인 + if (pot == null || user == null) { + throw new IllegalArgumentException("Pot or User cannot be null"); + } + + return PotApplication.builder() + .pot(pot) + .user(user) + .potRole(Role.valueOf(dto.getPotRole())) + .liked(false) // 기본값 false + .status(ApplicationStatus.PENDING) // 지원 상태 + .appliedAt(LocalDateTime.now()) // 지원 시간 + .build(); + } + + @Override + public PotApplicationResponseDto toDto(PotApplication entity) { + // Null 검사 및 예외 처리 + if (entity == null) { + throw new IllegalArgumentException("PotApplication entity cannot be null"); + } + if (entity.getPot() == null) { + throw new IllegalArgumentException("Pot entity cannot be null in PotApplication"); + } + if (entity.getUser() == null) { + throw new IllegalArgumentException("User entity cannot be null in PotApplication"); + } + + return PotApplicationResponseDto.builder() + .applicationId(entity.getApplicationId()) + .potRole(entity.getPotRole().name()) + .liked(entity.getLiked()) + .status(entity.getStatus() != null ? entity.getStatus().name() : "UNKNOWN") // 상태가 null이면 기본값 설정 + .appliedAt(entity.getAppliedAt()) // null 가능성을 허용 + .potId(entity.getPot().getPotId()) // Pot 엔티티에서 potId 가져오기 + .userId(entity.getUser().getId()) // User 엔티티에서 id 가져오기 + .userNickname(entity.getUser().getNickname() + getVegetableNameByRole(String.valueOf(entity.getUser().getRole()))) + .build(); + } + + private String getVegetableNameByRole(String role) { + Map roleToVegetableMap = Map.of( + "DESIGN", " 브로콜리", + "PLANNING", " 당근", + "BACKEND", " 양파", + "FRONTEND", " 버섯" + ); + + return roleToVegetableMap.getOrDefault(role, "알 수 없음"); + } + +} diff --git a/src/main/java/stackpot/stackpot/converter/PotConverter.java b/src/main/java/stackpot/stackpot/converter/PotConverter.java new file mode 100644 index 00000000..750157af --- /dev/null +++ b/src/main/java/stackpot/stackpot/converter/PotConverter.java @@ -0,0 +1,19 @@ +package stackpot.stackpot.converter; + +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.PotRecruitmentDetails; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.web.dto.CompletedPotResponseDto; +import stackpot.stackpot.web.dto.PotRequestDto; +import stackpot.stackpot.web.dto.PotResponseDto; +import stackpot.stackpot.web.dto.PotSearchResponseDto; + +import java.util.List; +import java.util.Map; + +public interface PotConverter { + Pot toEntity(PotRequestDto dto,User user); + CompletedPotResponseDto toCompletedPotResponseDto(Pot pot, Map roleCounts); + PotResponseDto toDto(Pot entity, List recruitmentDetails); + PotSearchResponseDto toSearchDto(Pot pot); +} diff --git a/src/main/java/stackpot/stackpot/converter/PotConverterImpl.java b/src/main/java/stackpot/stackpot/converter/PotConverterImpl.java new file mode 100644 index 00000000..bb750dd1 --- /dev/null +++ b/src/main/java/stackpot/stackpot/converter/PotConverterImpl.java @@ -0,0 +1,131 @@ +package stackpot.stackpot.converter; + +import org.springframework.stereotype.Component; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.PotRecruitmentDetails; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.enums.PotModeOfOperation; +import stackpot.stackpot.web.dto.*; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import java.time.format.DateTimeFormatter; + +@Component +public class PotConverterImpl implements PotConverter { + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + + @Override + public Pot toEntity(PotRequestDto requestDto, User user) { + return Pot.builder() + .potName(requestDto.getPotName()) + .potStartDate(requestDto.getPotStartDate()) + .potEndDate(requestDto.getPotEndDate()) + .potDuration(requestDto.getPotDuration()) + .potLan(requestDto.getPotLan()) + .potContent(requestDto.getPotContent()) + .potStatus(requestDto.getPotStatus()) + .potModeOfOperation(PotModeOfOperation.valueOf(requestDto.getPotModeOfOperation())) + .potSummary(requestDto.getPotSummary()) + .recruitmentDeadline(requestDto.getRecruitmentDeadline()) + .user(user) // 사용자 설정 + .build(); + } + + @Override + public PotResponseDto toDto(Pot entity, List recruitmentDetails) { + return PotResponseDto.builder() + .potId(entity.getPotId()) + .potName(entity.getPotName()) + .potStartDate(formatDate(entity.getPotStartDate())) + .potEndDate(formatDate(entity.getPotEndDate())) + .potDuration(entity.getPotDuration()) + .potLan(entity.getPotLan()) + .potContent(entity.getPotContent()) + .potStatus(entity.getPotStatus()) + .potModeOfOperation(entity.getPotModeOfOperation().name()) + .potSummary(entity.getPotSummary()) + .recruitmentDeadline(entity.getRecruitmentDeadline()) + .recruitmentDetails(recruitmentDetails.stream().map(r -> PotRecruitmentResponseDto.builder() + .recruitmentId(r.getRecruitmentId()) + .recruitmentRole(r.getRecruitmentRole().name()) + .recruitmentCount(r.getRecruitmentCount()) + .build()).collect(Collectors.toList())) + .build(); + } + + private String formatDate(java.time.LocalDate date) { + return (date != null) ? date.format(DATE_FORMATTER) : "N/A"; + } + + @Override + + public CompletedPotResponseDto toCompletedPotResponseDto(Pot pot, Map roleCounts) { + return CompletedPotResponseDto.builder() + .potId(pot.getPotId()) + .potName(pot.getPotName()) + .potStartDate(pot.getPotStartDate()) + .potEndDate(pot.getPotEndDate()) + .potDuration(pot.getPotDuration()) + .potLan(pot.getPotLan()) + .potContent(pot.getPotContent()) + .potStatus(pot.getPotStatus()) + .potModeOfOperation(pot.getPotModeOfOperation()) + .potSummary(pot.getPotSummary()) + .recruitmentDeadline(pot.getRecruitmentDeadline()) + .recruitmentDetails(pot.getRecruitmentDetails().stream() + .map(rd -> RecruitmentDetailResponseDto.builder() + .recruitmentRole(String.valueOf(rd.getRecruitmentRole())) + .recruitmentCount(rd.getRecruitmentCount()) + .build()) + .collect(Collectors.toList())) + .roleCounts(roleCounts) + .build(); + } + + public PotSearchResponseDto toSearchDto(Pot pot) { + // 역할 이름 매핑 (유효한 역할만 처리) + String roleName = pot.getUser() != null && pot.getUser().getRole() != null + ? pot.getUser().getRole().name() + : "멤버"; + + String nicknameWithRole = pot.getUser() != null && pot.getUser().getNickname() != null + ? pot.getUser().getNickname() + " " + mapRoleName(roleName) + : "Unknown 멤버"; + + return PotSearchResponseDto.builder() + .potId(pot.getPotId()) + .potName(pot.getPotName()) + .potContent(pot.getPotContent()) + .creatorNickname(nicknameWithRole) + .creatorRole( + pot.getUser() != null && pot.getUser().getRole() != null + ? pot.getUser().getRole().name() + : "멤버" // 기본값 설정 + ) + .recruitmentPart( + pot.getRecruitmentDetails() != null + ? pot.getRecruitmentDetails().stream() + .filter(rd -> rd.getRecruitmentRole() != null) + .map(rd -> rd.getRecruitmentRole().name()) + .collect(Collectors.joining(", ")) + : "없음" // 기본값 설정 + ) + .recruitmentDeadline(pot.getRecruitmentDeadline()) + .build(); + } + + + private String mapRoleName(String roleName) { + return switch (roleName) { + case "BACKEND" -> "양파"; + case "FRONTEND" -> "버섯"; + case "DESIGN" -> "브로콜리"; + case "PLANNING" -> "당근"; + default -> "멤버"; + }; + } +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/converter/PotMemberConverter/PotMemberConverter.java b/src/main/java/stackpot/stackpot/converter/PotMemberConverter/PotMemberConverter.java new file mode 100644 index 00000000..d3efeb31 --- /dev/null +++ b/src/main/java/stackpot/stackpot/converter/PotMemberConverter/PotMemberConverter.java @@ -0,0 +1,13 @@ +package stackpot.stackpot.converter.PotMemberConverter; + +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.mapping.PotApplication; +import stackpot.stackpot.domain.mapping.PotMember; +import stackpot.stackpot.web.dto.PotMemberAppealResponseDto; + +public interface PotMemberConverter { + PotMember toEntity(User user, Pot pot, PotApplication application, Boolean isOwner); + PotMemberAppealResponseDto toDto(PotMember entity); + +} diff --git a/src/main/java/stackpot/stackpot/converter/PotMemberConverter/PotMemberConverterImpl.java b/src/main/java/stackpot/stackpot/converter/PotMemberConverter/PotMemberConverterImpl.java new file mode 100644 index 00000000..b8ae7f26 --- /dev/null +++ b/src/main/java/stackpot/stackpot/converter/PotMemberConverter/PotMemberConverterImpl.java @@ -0,0 +1,60 @@ +package stackpot.stackpot.converter.PotMemberConverter; + +import org.springframework.stereotype.Component; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.mapping.PotApplication; +import stackpot.stackpot.domain.mapping.PotMember; +import stackpot.stackpot.web.dto.PotMemberAppealResponseDto; + +@Component +public class PotMemberConverterImpl implements PotMemberConverter { + + @Override + public PotMember toEntity(User user, Pot pot, PotApplication application, Boolean isOwner) { + return PotMember.builder() + .user(user) + .pot(pot) + .potApplication(application) + .roleName(application != null ? application.getPotRole() : null) // PotRole Enum 그대로 사용 + .owner(isOwner) + .appealContent(null) + .build(); + } + + @Override + + public PotMemberAppealResponseDto toDto(PotMember entity) { + String roleName = entity.getRoleName() != null ? entity.getRoleName().name() : "멤버"; + String nicknameWithRole = entity.getUser().getNickname() + " "+mapRoleName(roleName) ; + + return PotMemberAppealResponseDto.builder() + .potMemberId(entity.getPotMemberId()) + .potId(entity.getPot().getPotId()) + .userId(entity.getUser().getId()) + .roleName(roleName) + .nickname(nicknameWithRole) + .isOwner(entity.isOwner()) + .appealContent(entity.getAppealContent()) + .build(); + } + + + private String mapRoleName(String potRole) { + switch (potRole) { + case "BACKEND": + return "양파"; + case "FRONTEND": + return "버섯"; + case "DESIGN": + return "브로콜리"; + case "PLANNING": + return "당근"; + default: + return "멤버"; + } + } + + + +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/converter/TaskboardConverter.java b/src/main/java/stackpot/stackpot/converter/TaskboardConverter.java new file mode 100644 index 00000000..647f472f --- /dev/null +++ b/src/main/java/stackpot/stackpot/converter/TaskboardConverter.java @@ -0,0 +1,18 @@ +package stackpot.stackpot.converter; + +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.Taskboard; +import stackpot.stackpot.domain.mapping.PotMember; +import stackpot.stackpot.domain.mapping.Task; +import stackpot.stackpot.repository.TaskRepository; +import stackpot.stackpot.web.dto.MyPotTaskRequestDto; +import stackpot.stackpot.web.dto.MyPotTaskResponseDto; + +import java.util.List; + +public interface TaskboardConverter { + Taskboard toTaskboard(Pot pot , MyPotTaskRequestDto.create requset); + MyPotTaskResponseDto toDTO(Taskboard taskboard); + List toParticipantDtoList(List participants); + MyPotTaskResponseDto.Participant toParticipantDto(PotMember participant); +} diff --git a/src/main/java/stackpot/stackpot/converter/TaskboardConverterImpl.java b/src/main/java/stackpot/stackpot/converter/TaskboardConverterImpl.java new file mode 100644 index 00000000..4f5bcc78 --- /dev/null +++ b/src/main/java/stackpot/stackpot/converter/TaskboardConverterImpl.java @@ -0,0 +1,64 @@ +package stackpot.stackpot.converter; + +import org.springframework.stereotype.Component; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.Taskboard; +import stackpot.stackpot.domain.mapping.PotMember; +import stackpot.stackpot.web.dto.MyPotTaskRequestDto; +import stackpot.stackpot.web.dto.MyPotTaskResponseDto; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component +public class TaskboardConverterImpl implements TaskboardConverter{ + @Override + public Taskboard toTaskboard(Pot pot, MyPotTaskRequestDto.create requset) { + return Taskboard.builder() + .title(requset.getTitle()) + .description(requset.getDescription()) + .endDate(requset.getDeadline()) + .startDate(requset.getDeadline()) + .status(requset.getTaskboardStatus()) + .pot(pot) + .build(); + } + @Override + public MyPotTaskResponseDto toDTO(Taskboard taskboard) { + return MyPotTaskResponseDto.builder() + .taskboardId(taskboard.getTaskboardId()) + .title(taskboard.getTitle()) + .description(taskboard.getDescription()) + .status(taskboard.getStatus()) + .potId(taskboard.getPot().getPotId()) + .build(); + } + @Override + + public List toParticipantDtoList(List participants) { + return participants.stream() + .map(this::toParticipantDto) + .collect(Collectors.toList()); + } + + @Override + public MyPotTaskResponseDto.Participant toParticipantDto(PotMember participant) { + + return MyPotTaskResponseDto.Participant.builder() + .potMemberId(participant.getPotMemberId()) + .userId(participant.getUser().getUserId()) + .nickName(participant.getUser().getNickname() + " " + getVegetableNameByRole(participant.getRoleName().toString())) + .build(); + } + + private String getVegetableNameByRole(String role) { + Map roleToVegetableMap = Map.of( + "BACKEND", " 양파", + "FRONTEND", " 버섯", + "DESIGN", " 브로콜리", + "PLANNING", " 당근" + ); + return roleToVegetableMap.getOrDefault(role, "알 수 없음"); + } +} diff --git a/src/main/java/stackpot/stackpot/converter/UserConverter.java b/src/main/java/stackpot/stackpot/converter/UserConverter.java new file mode 100644 index 00000000..e32e565f --- /dev/null +++ b/src/main/java/stackpot/stackpot/converter/UserConverter.java @@ -0,0 +1,52 @@ +package stackpot.stackpot.converter; + +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.enums.Role; +import stackpot.stackpot.web.dto.UserRequestDto; +import stackpot.stackpot.web.dto.UserResponseDto; + +import java.util.Map; + +public class UserConverter { + public static User toUser(UserRequestDto.JoinDto request) { + + return User.builder() + .nickname(request.getNickname()) + .kakaoId(request.getKakaoId()) + .interest(request.getInterest()) + .role(Role.valueOf(String.valueOf(request.getRole()))) + .build(); + } + + public static UserResponseDto toDto(User user) { + + if (user.getId() == null) { + throw new IllegalStateException("User ID is null"); + } + + // 역할명을 변환하여 닉네임에 추가 + String roleName = user.getRole() != null ? user.getRole().name() : "멤버"; + String nicknameWithRole = user.getNickname() + " " + toDtoRole(roleName); + + return UserResponseDto.builder() + .id(user.getId()) // id 값이 제대로 설정되었는지 로그 확인 + .nickname(nicknameWithRole) + .email(user.getEmail()) + .kakaoId(user.getKakaoId()) + .role(user.getRole()) + .interest(user.getInterest()) + .userTemperature(user.getUserTemperature()) + .build(); + } + + public static String toDtoRole(String roleName) { + return switch (roleName) { + case "BACKEND" -> "양파"; + case "FRONTEND" -> "버섯"; + case "DESIGN" -> "브로콜리"; + case "PLANNING" -> "당근"; + default -> "멤버"; + }; + } +} + diff --git a/src/main/java/stackpot/stackpot/converter/UserMypageConverter.java b/src/main/java/stackpot/stackpot/converter/UserMypageConverter.java new file mode 100644 index 00000000..4d3627a8 --- /dev/null +++ b/src/main/java/stackpot/stackpot/converter/UserMypageConverter.java @@ -0,0 +1,99 @@ +package stackpot.stackpot.converter; + +import org.springframework.stereotype.Component; +import stackpot.stackpot.domain.Feed; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.repository.FeedLikeRepository; +import stackpot.stackpot.web.dto.PotRecruitmentResponseDto; +import stackpot.stackpot.web.dto.UserMypageResponseDto; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static com.mysql.cj.util.TimeUtil.DATE_FORMATTER; + +@Component +public class UserMypageConverter { + + public UserMypageResponseDto toDto(User user, List completedPots, List feeds) { + return UserMypageResponseDto.builder() + .id(user.getId()) + .email(user.getEmail()) + .nickname(user.getNickname() + getVegetableNameByRole(user.getRole().name())) + .role(user.getRole()) + .interest(user.getInterest()) + .userTemperature(user.getUserTemperature()) + .kakaoId(user.getKakaoId()) + .userIntroduction(user.getUserIntroduction()) + .completedPots(completedPots.stream().map(this::convertToCompletedPotDto).collect(Collectors.toList())) + .feeds(feeds.stream().map(this::convertToFeedDto).collect(Collectors.toList())) + .build(); + } + + + private UserMypageResponseDto.CompletedPotDto convertToCompletedPotDto(Pot pot) { + // 역할별 인원 수 집계 + Map roleSummary = pot.getPotMembers().stream() + .collect(Collectors.groupingBy( + member -> member.getRoleName().name(), // Enum을 String으로 변환 + Collectors.counting() + )); + + + return UserMypageResponseDto.CompletedPotDto.builder() + .potId(pot.getPotId()) + .potName(pot.getPotName()) + .potStartDate(formatDate(pot.getPotStartDate())) + .potEndDate(formatDate(pot.getPotEndDate())) + .potSummary(pot.getPotSummary()) + .recruitmentDetails(pot.getRecruitmentDetails().stream() + .map(detail -> PotRecruitmentResponseDto.builder() + .recruitmentId(detail.getRecruitmentId()) + .recruitmentRole(detail.getRecruitmentRole().name()) // Enum → String 변환 + .recruitmentCount(detail.getRecruitmentCount()) + .build()) + .collect(Collectors.toList())) + .build(); + } + + private final FeedLikeRepository feedLikeRepository; + + public UserMypageConverter(FeedLikeRepository feedLikeRepository) { + this.feedLikeRepository = feedLikeRepository; + } + + private UserMypageResponseDto.FeedDto convertToFeedDto(Feed feed) { + return UserMypageResponseDto.FeedDto.builder() + .feedId(feed.getFeedId()) + .title(feed.getTitle()) + .content(feed.getContent()) + .category(feed.getCategory()) + .likeCount(feedLikeRepository.countByFeed(feed)) + .createdAt(formatLocalDateTime(feed.getCreatedAt())) + .build(); + } + + private String getVegetableNameByRole(String role) { + Map roleToVegetableMap = Map.of( + "BACKEND", " 양파", + "FRONTEND", " 버섯", + "DESIGN", " 브로콜리", + "PLANNING", " 당근" + ); + return roleToVegetableMap.getOrDefault(role, "알 수 없음"); + } + + private String formatDate(java.time.LocalDate date) { + return (date != null) ? date.format(DATE_FORMATTER) : "N/A"; + } + + // 날짜 포맷 적용 메서드 + private String formatLocalDateTime(LocalDateTime dateTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 M월 d일 H:mm"); + return (dateTime != null) ? dateTime.format(formatter) : "날짜 없음"; + } +} diff --git a/src/main/java/stackpot/stackpot/domain/Badge.java b/src/main/java/stackpot/stackpot/domain/Badge.java new file mode 100644 index 00000000..d1d691a7 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/Badge.java @@ -0,0 +1,23 @@ +package stackpot.stackpot.domain; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Badge extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long badgeId; + + @Column(nullable = false, length = 30) + private String name; + + @Column(nullable = false, length = 50) + private String description; +} diff --git a/src/main/java/stackpot/stackpot/domain/Feed.java b/src/main/java/stackpot/stackpot/domain/Feed.java new file mode 100644 index 00000000..ca700da6 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/Feed.java @@ -0,0 +1,38 @@ +package stackpot.stackpot.domain; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.common.BaseEntity; +import stackpot.stackpot.domain.enums.Category; +import stackpot.stackpot.domain.enums.Role; +import stackpot.stackpot.domain.enums.Visibility; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Feed extends BaseEntity{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long feedId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false, length = 50) + private String title; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Category category; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Visibility visibility; +} diff --git a/src/main/java/stackpot/stackpot/domain/FeedFile.java b/src/main/java/stackpot/stackpot/domain/FeedFile.java new file mode 100644 index 00000000..6555d4b3 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/FeedFile.java @@ -0,0 +1,24 @@ +package stackpot.stackpot.domain; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.common.BaseEntity; +import stackpot.stackpot.domain.enums.Visibility; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class FeedFile extends BaseEntity{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long fileId; + + @Column(length = 255, nullable = false) + private String fileUrl; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "feed_id", nullable = false) + private Feed feed; +} diff --git a/src/main/java/stackpot/stackpot/domain/Pot.java b/src/main/java/stackpot/stackpot/domain/Pot.java new file mode 100644 index 00000000..4da17c64 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/Pot.java @@ -0,0 +1,94 @@ +package stackpot.stackpot.domain; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.common.BaseEntity; +import stackpot.stackpot.domain.enums.PotModeOfOperation; +import stackpot.stackpot.domain.mapping.PotApplication; +import stackpot.stackpot.domain.mapping.PotMember; + +import java.time.LocalDate; +import java.util.Map; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Pot extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long potId; + + @Setter + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @OneToMany(mappedBy = "pot") + private List recruitmentDetails; + + @OneToMany(mappedBy = "pot", cascade = CascadeType.ALL, orphanRemoval = true) + private List potApplication; + + @OneToMany(mappedBy = "pot", cascade = CascadeType.ALL, orphanRemoval = true) + private List potMembers; + + @Column(nullable = false, length = 255) + private String potName; + + @Setter + @Column(nullable = false) + private LocalDate potStartDate; + + @Column(nullable = false) + private LocalDate potEndDate; + + @Column(nullable = false, length = 255) + private String potDuration; + + @Column(nullable = false, length = 255) + private String potLan; + + @Column(nullable = true, columnDefinition = "TEXT") + private String potContent; + + @Setter + @Column(nullable = false, length = 255) + private String potStatus; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PotModeOfOperation potModeOfOperation; // 팟 진행 방식 + + @Column(nullable = true, length = 400) + private String potSummary; // 팟 요약 + + @Column(nullable = false) + private LocalDate recruitmentDeadline; + public void updateFields(Map updates) { + updates.forEach((key, value) -> { + if (value != null) { + switch (key) { + case "potName" -> this.potName = (String) value; + case "potStartDate" -> this.potStartDate = (LocalDate) value; + case "potEndDate" -> this.potEndDate = (LocalDate) value; + case "potDuration" -> this.potDuration = (String) value; + case "potLan" -> this.potLan = (String) value; + case "potContent" -> this.potContent = (String) value; + case "potStatus" -> this.potStatus = (String) value; + case "potModeOfOperation" -> this.potModeOfOperation = PotModeOfOperation.valueOf((String) value); + case "potSummary" -> this.potSummary = (String) value; + case "recruitmentDeadline" -> this.recruitmentDeadline = (LocalDate) value; + } + } + }); + } + + + + +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/domain/PotDocument.java b/src/main/java/stackpot/stackpot/domain/PotDocument.java new file mode 100644 index 00000000..9ee29a52 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/PotDocument.java @@ -0,0 +1,25 @@ +package stackpot.stackpot.domain; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class PotDocument extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long documentId; + + @Column(nullable = false, length = 255) + private String documentUrl; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "pot_id", nullable = false) + private Pot pot; +} diff --git a/src/main/java/stackpot/stackpot/domain/PotOpeningImg.java b/src/main/java/stackpot/stackpot/domain/PotOpeningImg.java new file mode 100644 index 00000000..0cc44869 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/PotOpeningImg.java @@ -0,0 +1,25 @@ +package stackpot.stackpot.domain; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class PotOpeningImg extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long imgId; + + @Column(nullable = false, length = 255) + private String imgUrl; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "pot_id", nullable = false) + private Pot pot; +} diff --git a/src/main/java/stackpot/stackpot/domain/PotRecruitmentDetails.java b/src/main/java/stackpot/stackpot/domain/PotRecruitmentDetails.java new file mode 100644 index 00000000..52496a42 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/PotRecruitmentDetails.java @@ -0,0 +1,37 @@ +package stackpot.stackpot.domain; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.common.BaseEntity; +import stackpot.stackpot.domain.enums.Role; + + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class PotRecruitmentDetails extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + @Getter + @Setter + private Long recruitmentId; + + @Column(nullable = true, length = 255) + @Getter + @Setter + @Enumerated(EnumType.STRING) + private Role recruitmentRole; + + @Column(nullable = true) + @Getter + @Setter + private Integer recruitmentCount; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "pot_id", nullable = false) + private Pot pot; +} diff --git a/src/main/java/stackpot/stackpot/domain/Taskboard.java b/src/main/java/stackpot/stackpot/domain/Taskboard.java new file mode 100644 index 00000000..4a12daaf --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/Taskboard.java @@ -0,0 +1,43 @@ +package stackpot.stackpot.domain; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.common.BaseEntity; +import stackpot.stackpot.domain.enums.TaskboardStatus; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Taskboard extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long taskboardId; + + @Column(nullable = false, length = 20) + private String title; + + @Column(nullable = true, columnDefinition = "TEXT") + private String description; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private TaskboardStatus status; + + @Column(nullable = false) + private LocalDateTime startDate; + + @Column(nullable = false) + private LocalDateTime endDate; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "pot_id", nullable = false) + private Pot pot; + +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/domain/User.java b/src/main/java/stackpot/stackpot/domain/User.java new file mode 100644 index 00000000..e2fdc85a --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/User.java @@ -0,0 +1,73 @@ +package stackpot.stackpot.domain; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import stackpot.stackpot.domain.common.BaseEntity; +import stackpot.stackpot.domain.enums.Role; + +import java.util.Collection; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class User extends BaseEntity implements UserDetails{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // Primary Key + +// @Column(nullable = false, length = 255) +// private String loginId; // 로그인 아이디 +// +//// @Column(nullable = true, length = 12) +// private String userName; // 유저 카톡 설정 이름 + +// @Column(nullable = false, length = 255) +// private String snsKey; // SNS 키 + + @Column(nullable = true, length = 255) + private String nickname; // 닉네임 + + @Enumerated(EnumType.STRING) + @Column(nullable = true, length = 255) + private Role role; // 역할 + + @Column(nullable = true, length = 255) + private String interest; // 관심사 + + @Column(nullable = true, columnDefinition = "TEXT") + private String userIntroduction; // 한 줄 소개 + + @Column(nullable = true) + private Integer userTemperature; // 유저 온도 + + @Column(nullable = false, unique = true) + private String email; // 이메일 + + @Column(nullable = true, unique = true) + private String kakaoId; + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return email; // 사용자 식별자로 이메일을 사용 + } + public Long getUserId() { + return id; + } + +} + diff --git a/src/main/java/stackpot/stackpot/domain/Uuid.java b/src/main/java/stackpot/stackpot/domain/Uuid.java new file mode 100644 index 00000000..52c81fec --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/Uuid.java @@ -0,0 +1,21 @@ +package stackpot.stackpot.domain; + +import stackpot.stackpot.domain.common.BaseEntity; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Uuid extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // Primary Key + + @Column(unique = true) + private String uuid; +} diff --git a/src/main/java/stackpot/stackpot/domain/common/BaseEntity.java b/src/main/java/stackpot/stackpot/domain/common/BaseEntity.java new file mode 100644 index 00000000..cdf0ee14 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/common/BaseEntity.java @@ -0,0 +1,22 @@ +package stackpot.stackpot.domain.common; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +public abstract class BaseEntity { + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} + diff --git a/src/main/java/stackpot/stackpot/domain/enums/ApplicationStatus.java b/src/main/java/stackpot/stackpot/domain/enums/ApplicationStatus.java new file mode 100644 index 00000000..4577b06e --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/enums/ApplicationStatus.java @@ -0,0 +1,5 @@ +package stackpot.stackpot.domain.enums; + +public enum ApplicationStatus { + PENDING, APPROVED, REJECTED +} diff --git a/src/main/java/stackpot/stackpot/domain/enums/Category.java b/src/main/java/stackpot/stackpot/domain/enums/Category.java new file mode 100644 index 00000000..11160734 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/enums/Category.java @@ -0,0 +1,11 @@ +package stackpot.stackpot.domain.enums; + +public enum Category { + + ALL, + BACKEND, + FRONTEND, + DESIGN, + PLANNING + +} diff --git a/src/main/java/stackpot/stackpot/domain/enums/PotModeOfOperation.java b/src/main/java/stackpot/stackpot/domain/enums/PotModeOfOperation.java new file mode 100644 index 00000000..c07b1e07 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/enums/PotModeOfOperation.java @@ -0,0 +1,5 @@ +package stackpot.stackpot.domain.enums; + +public enum PotModeOfOperation { + ONLINE, OFFLINE +} diff --git a/src/main/java/stackpot/stackpot/domain/enums/Role.java b/src/main/java/stackpot/stackpot/domain/enums/Role.java new file mode 100644 index 00000000..f3617965 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/enums/Role.java @@ -0,0 +1,8 @@ +package stackpot.stackpot.domain.enums; + +public enum Role { + BACKEND, + FRONTEND, + DESIGN, + PLANNING, +} diff --git a/src/main/java/stackpot/stackpot/domain/enums/TaskboardStatus.java b/src/main/java/stackpot/stackpot/domain/enums/TaskboardStatus.java new file mode 100644 index 00000000..5130f34a --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/enums/TaskboardStatus.java @@ -0,0 +1,5 @@ +package stackpot.stackpot.domain.enums; + +public enum TaskboardStatus { + OPEN, IN_PROGRESS, CLOSED +} diff --git a/src/main/java/stackpot/stackpot/domain/enums/TodoStatus.java b/src/main/java/stackpot/stackpot/domain/enums/TodoStatus.java new file mode 100644 index 00000000..df5315a4 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/enums/TodoStatus.java @@ -0,0 +1,5 @@ +package stackpot.stackpot.domain.enums; + +public enum TodoStatus { + NOT_STARTED, COMPLETED +} diff --git a/src/main/java/stackpot/stackpot/domain/enums/Visibility.java b/src/main/java/stackpot/stackpot/domain/enums/Visibility.java new file mode 100644 index 00000000..6deea23d --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/enums/Visibility.java @@ -0,0 +1,5 @@ +package stackpot.stackpot.domain.enums; + +public enum Visibility { + PRIVATE, PUBLIC +} diff --git a/src/main/java/stackpot/stackpot/domain/mapping/FeedLike.java b/src/main/java/stackpot/stackpot/domain/mapping/FeedLike.java new file mode 100644 index 00000000..79fc96f7 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/mapping/FeedLike.java @@ -0,0 +1,27 @@ +package stackpot.stackpot.domain.mapping; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.Feed; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.common.BaseEntity; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class FeedLike extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long likeId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "feed_id", nullable = false) + private Feed feed; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; +} diff --git a/src/main/java/stackpot/stackpot/domain/mapping/FeedSave.java b/src/main/java/stackpot/stackpot/domain/mapping/FeedSave.java new file mode 100644 index 00000000..8b37062c --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/mapping/FeedSave.java @@ -0,0 +1,28 @@ +package stackpot.stackpot.domain.mapping; + +import jakarta.persistence.*; +import stackpot.stackpot.domain.Feed; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.common.BaseEntity; + +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class FeedSave extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long feedSaveId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "feed_id", nullable = false) + private Feed feed; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; +} diff --git a/src/main/java/stackpot/stackpot/domain/mapping/PotApplication.java b/src/main/java/stackpot/stackpot/domain/mapping/PotApplication.java new file mode 100644 index 00000000..b7ca450d --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/mapping/PotApplication.java @@ -0,0 +1,57 @@ +package stackpot.stackpot.domain.mapping; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.common.BaseEntity; +import stackpot.stackpot.domain.enums.ApplicationStatus; +import stackpot.stackpot.domain.enums.Role; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class PotApplication extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // IDENTITY로 자동 증가 설정 + @Column(name = "application_id", nullable = false) + private Long applicationId; // Primary Key + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ApplicationStatus status; + + @Column(nullable = true) + private LocalDateTime appliedAt; + + @Setter + @Column(nullable = false) + @Builder.Default + private Boolean liked = false; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 10) + private Role potRole; // 팟 역할 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "pot_id", nullable = false) + private Pot pot; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false) + private User user; + + public void setApplicationStatus(ApplicationStatus status) { + this.status = status; + } + + @OneToOne(mappedBy = "potApplication", cascade = CascadeType.ALL, orphanRemoval = true) + private PotMember potMember; + +} diff --git a/src/main/java/stackpot/stackpot/domain/mapping/PotMember.java b/src/main/java/stackpot/stackpot/domain/mapping/PotMember.java new file mode 100644 index 00000000..3c00ef49 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/mapping/PotMember.java @@ -0,0 +1,46 @@ +package stackpot.stackpot.domain.mapping; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.common.BaseEntity; +import stackpot.stackpot.domain.enums.Role; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class PotMember extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long potMemberId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "pot_id", nullable = false) + private Pot pot; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "application_id", nullable = false) + private PotApplication potApplication; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 10) + private Role roleName; + + @Getter + @Column(nullable = false) + private boolean owner; + + @Setter + @Getter + @Column(nullable = true) + private String appealContent; +} diff --git a/src/main/java/stackpot/stackpot/domain/mapping/PotMemberBadge.java b/src/main/java/stackpot/stackpot/domain/mapping/PotMemberBadge.java new file mode 100644 index 00000000..da7e1022 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/mapping/PotMemberBadge.java @@ -0,0 +1,27 @@ +package stackpot.stackpot.domain.mapping; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.Badge; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class PotMemberBadge extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long potMemberBadgeId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "badge_id", nullable = false) + private Badge badge; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "pot_member_id", nullable = false) + private PotMember potMember; +} diff --git a/src/main/java/stackpot/stackpot/domain/mapping/PotSave.java b/src/main/java/stackpot/stackpot/domain/mapping/PotSave.java new file mode 100644 index 00000000..13e86afd --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/mapping/PotSave.java @@ -0,0 +1,28 @@ +package stackpot.stackpot.domain.mapping; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class PotSave extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long potSaveId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "pot_id", nullable = false) + private Pot pot; +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/domain/mapping/Task.java b/src/main/java/stackpot/stackpot/domain/mapping/Task.java new file mode 100644 index 00000000..32cd5a7c --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/mapping/Task.java @@ -0,0 +1,27 @@ +package stackpot.stackpot.domain.mapping; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.Taskboard; +import stackpot.stackpot.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Task extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long taskId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "taskboard_id", nullable = false) + private Taskboard taskboard; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "potMember_id", nullable = false) + private PotMember potMember; +} diff --git a/src/main/java/stackpot/stackpot/domain/mapping/TaskComment.java b/src/main/java/stackpot/stackpot/domain/mapping/TaskComment.java new file mode 100644 index 00000000..a0ec30b5 --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/mapping/TaskComment.java @@ -0,0 +1,30 @@ +package stackpot.stackpot.domain.mapping; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.Taskboard; +import stackpot.stackpot.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class TaskComment extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long commentId; + + @Column(nullable = false, columnDefinition = "TEXT") + private String comment_content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "potMember_id", nullable = false) + private PotMember potMember; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "taskboard_id", nullable = false) + private Taskboard taskboard; +} diff --git a/src/main/java/stackpot/stackpot/domain/mapping/UserTodo.java b/src/main/java/stackpot/stackpot/domain/mapping/UserTodo.java new file mode 100644 index 00000000..08bec2ed --- /dev/null +++ b/src/main/java/stackpot/stackpot/domain/mapping/UserTodo.java @@ -0,0 +1,37 @@ +package stackpot.stackpot.domain.mapping; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.common.BaseEntity; +import stackpot.stackpot.domain.enums.TodoStatus; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class UserTodo extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long todoId; + + @Column(nullable = false, length = 50) + private String content; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private TodoStatus status; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "pot_id", nullable = false) + private Pot pot; +} diff --git a/src/main/java/stackpot/stackpot/repository/FeedLikeRepository.java b/src/main/java/stackpot/stackpot/repository/FeedLikeRepository.java new file mode 100644 index 00000000..cd198575 --- /dev/null +++ b/src/main/java/stackpot/stackpot/repository/FeedLikeRepository.java @@ -0,0 +1,22 @@ +package stackpot.stackpot.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import stackpot.stackpot.domain.Feed; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.mapping.FeedLike; + +import java.util.Optional; + +@Repository +public interface FeedLikeRepository extends JpaRepository { + + // 특정 사용자가 특정 게시물에 좋아요를 눌렀는지 확인 + Optional findByFeedAndUser(Feed feed, User user); + + // 특정 게시물의 좋아요 개수 조회 + @Query("SELECT COUNT(fl) FROM FeedLike fl WHERE fl.feed = :feed") + Long countByFeed(@Param("feed") Feed feed); +} diff --git a/src/main/java/stackpot/stackpot/repository/FeedRepository/FeedRepository.java b/src/main/java/stackpot/stackpot/repository/FeedRepository/FeedRepository.java new file mode 100644 index 00000000..f377ea79 --- /dev/null +++ b/src/main/java/stackpot/stackpot/repository/FeedRepository/FeedRepository.java @@ -0,0 +1,42 @@ +package stackpot.stackpot.repository.FeedRepository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import stackpot.stackpot.domain.Feed; +import stackpot.stackpot.domain.enums.Category; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public interface FeedRepository extends JpaRepository { + + @Query("SELECT f, " + + " (COALESCE(FL.likeCount, 0) + COALESCE(FS.saveCount, 0)) AS popularity, " + + " COALESCE(FL.likeCount, 0) AS likeCount " + + "FROM Feed f " + + "LEFT JOIN (SELECT fl.feed.id AS feedId, COUNT(fl) AS likeCount FROM FeedLike fl GROUP BY fl.feed.id) FL " + + "ON f.id = FL.feedId " + + "LEFT JOIN (SELECT fs.feed.id AS feedId, COUNT(fs) AS saveCount FROM FeedSave fs GROUP BY fs.feed.id) FS " + + "ON f.id = FS.feedId " + + "WHERE (:category IS NULL OR :category = '전체' OR f.category = :category) " + + "AND ((:sort = 'new' AND f.createdAt < :lastCreatedAt) " + + " OR (:sort = 'old' AND f.createdAt > :lastCreatedAt)) " + + "ORDER BY " + + "CASE WHEN :sort = 'popular' THEN (COALESCE(FL.likeCount, 0) + COALESCE(FS.saveCount, 0)) END DESC, " + + "CASE WHEN :sort = 'new' THEN f.createdAt END DESC, " + + "CASE WHEN :sort = 'old' THEN f.createdAt END ASC") + List findFeeds( + @Param("category") Category category, + @Param("sort") String sort, + @Param("lastCreatedAt") LocalDateTime lastCreatedAt, + Pageable pageable); + + List findByUser_Id(Long userId); + Page findByTitleContainingOrContentContainingOrderByCreatedAtDesc(String titleKeyword, String contentKeyword, Pageable pageable); + +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/repository/FeedSaveRepository.java b/src/main/java/stackpot/stackpot/repository/FeedSaveRepository.java new file mode 100644 index 00000000..f03e2dac --- /dev/null +++ b/src/main/java/stackpot/stackpot/repository/FeedSaveRepository.java @@ -0,0 +1,21 @@ +package stackpot.stackpot.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import stackpot.stackpot.domain.Feed; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.mapping.FeedSave; + +import java.util.Optional; + +@Repository +public interface FeedSaveRepository extends JpaRepository { + // 특정 사용자가 특정 게시물에 좋아요를 눌렀는지 확인 + Optional findByFeedAndUser(Feed feed, User user); + + @Query("SELECT COUNT(fl) FROM FeedSave fl WHERE fl.feed = :feed") + Long countByFeed(@Param("feed") Feed feed); +} + diff --git a/src/main/java/stackpot/stackpot/repository/PotApplicationRepository/PotApplicationRepository.java b/src/main/java/stackpot/stackpot/repository/PotApplicationRepository/PotApplicationRepository.java new file mode 100644 index 00000000..2b2a99a9 --- /dev/null +++ b/src/main/java/stackpot/stackpot/repository/PotApplicationRepository/PotApplicationRepository.java @@ -0,0 +1,12 @@ +package stackpot.stackpot.repository.PotApplicationRepository; + +import org.springframework.data.jpa.repository.JpaRepository; +import stackpot.stackpot.domain.mapping.PotApplication; + +import java.util.List; + +public interface PotApplicationRepository extends JpaRepository { + List findByPot_PotId(Long potId); + boolean existsByUserIdAndPot_PotId(Long userId, Long potId); + +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/repository/PotMemberRepository.java b/src/main/java/stackpot/stackpot/repository/PotMemberRepository.java new file mode 100644 index 00000000..727673cd --- /dev/null +++ b/src/main/java/stackpot/stackpot/repository/PotMemberRepository.java @@ -0,0 +1,32 @@ +package stackpot.stackpot.repository; + + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.mapping.PotMember; + +import java.util.List; +import java.util.Optional; + +@Repository +// 8. PotMemberRepository +public interface PotMemberRepository extends JpaRepository { + @Query("SELECT pm.user.id FROM PotMember pm WHERE pm.pot.potId = :potId") + List findUserIdsByPotId(@Param("potId") Long potId); + @Query("SELECT pm FROM PotMember pm WHERE pm.pot.potId = :potId") + List findByPotId(@Param("potId") Long potId); + @Query("SELECT pm.roleName, COUNT(pm) FROM PotMember pm WHERE pm.pot.potId = :potId GROUP BY pm.roleName") + List findRoleCountsByPotId(@Param("potId") Long potId); + + @Modifying + @Query("DELETE FROM PotMember pm WHERE pm.pot.potId = :potId AND pm.user.id = :userId") + void deleteByPotIdAndUserId(@Param("potId") Long potId, @Param("userId") Long userId); + + Optional findByPotAndUser(Pot pot, User user); + +} diff --git a/src/main/java/stackpot/stackpot/repository/PotRepository/MyPotRepository.java b/src/main/java/stackpot/stackpot/repository/PotRepository/MyPotRepository.java new file mode 100644 index 00000000..862cdda4 --- /dev/null +++ b/src/main/java/stackpot/stackpot/repository/PotRepository/MyPotRepository.java @@ -0,0 +1,16 @@ +package stackpot.stackpot.repository.PotRepository; + +import org.springframework.data.jpa.repository.JpaRepository; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.mapping.UserTodo; +import stackpot.stackpot.web.dto.MyPotTodoResponseDTO; + +import java.util.List; +import java.util.Optional; + +public interface MyPotRepository extends JpaRepository { + List findByPot_PotId(Long potId); + List findByPotAndUser(Pot pot, User user); + Optional findByTodoIdAndPot_PotId(Long todoId, Long potId); +} diff --git a/src/main/java/stackpot/stackpot/repository/PotRepository/PotRecruitmentDetailsRepository.java b/src/main/java/stackpot/stackpot/repository/PotRepository/PotRecruitmentDetailsRepository.java new file mode 100644 index 00000000..d3accf5d --- /dev/null +++ b/src/main/java/stackpot/stackpot/repository/PotRepository/PotRecruitmentDetailsRepository.java @@ -0,0 +1,15 @@ +package stackpot.stackpot.repository.PotRepository; + +import jakarta.transaction.Transactional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import stackpot.stackpot.domain.PotRecruitmentDetails; + +public interface PotRecruitmentDetailsRepository extends JpaRepository { + @Modifying + @Transactional + @Query("DELETE FROM PotRecruitmentDetails r WHERE r.pot.potId = :potId") + void deleteByPot_PotId(@Param("potId") Long potId); +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/repository/PotRepository/PotRepository.java b/src/main/java/stackpot/stackpot/repository/PotRepository/PotRepository.java new file mode 100644 index 00000000..694c696f --- /dev/null +++ b/src/main/java/stackpot/stackpot/repository/PotRepository/PotRepository.java @@ -0,0 +1,37 @@ +package stackpot.stackpot.repository.PotRepository; + + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.enums.Role; + +import java.util.List; +import java.util.Optional; + +public interface PotRepository extends JpaRepository { + Page findByRecruitmentDetails_RecruitmentRole(Role recruitmentRole, Pageable pageable); + Optional findPotWithRecruitmentDetailsByPotId(Long potId); + List findByPotApplication_User_Id(Long userId); + List findByUserId(Long userId); + Page findAll(Pageable pageable); + List findByPotMembers_UserIdAndPotStatus(Long userId, String status); + List findByUserIdAndPotStatus(Long userId, String status); + + @Query("SELECT p FROM Pot p " + + "WHERE LOWER(p.potName) LIKE LOWER(CONCAT('%', :keyword, '%')) " + + "OR LOWER(p.potContent) LIKE LOWER(CONCAT('%', :keyword, '%')) " + + "ORDER BY p.createdAt DESC") + Page searchByKeyword(@Param("keyword") String keyword, Pageable pageable); + + + @Query("SELECT p FROM Pot p WHERE p.potStatus = 'COMPLETED' " + + "AND (p.user.id = :userId OR p.potId IN " + + "(SELECT pm.pot.potId FROM PotMember pm WHERE pm.user.id = :userId)) " + + "AND (:cursor IS NULL OR p.potId < :cursor) " + + "ORDER BY p.potId DESC") + List findCompletedPotsByCursor(@Param("userId") Long userId, @Param("cursor") Long cursor); +} diff --git a/src/main/java/stackpot/stackpot/repository/TaskRepository.java b/src/main/java/stackpot/stackpot/repository/TaskRepository.java new file mode 100644 index 00000000..e35e468c --- /dev/null +++ b/src/main/java/stackpot/stackpot/repository/TaskRepository.java @@ -0,0 +1,15 @@ +package stackpot.stackpot.repository; + +import org.aspectj.apache.bcel.generic.LOOKUPSWITCH; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import stackpot.stackpot.domain.Taskboard; +import stackpot.stackpot.domain.mapping.Task; + +import java.util.List; + +@Repository +public interface TaskRepository extends JpaRepository { + List findByTaskboard(Taskboard taskboard); + +} diff --git a/src/main/java/stackpot/stackpot/repository/TaskboardRepository.java b/src/main/java/stackpot/stackpot/repository/TaskboardRepository.java new file mode 100644 index 00000000..a4a99702 --- /dev/null +++ b/src/main/java/stackpot/stackpot/repository/TaskboardRepository.java @@ -0,0 +1,8 @@ +package stackpot.stackpot.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import stackpot.stackpot.domain.Taskboard; +@Repository +public interface TaskboardRepository extends JpaRepository { +} diff --git a/src/main/java/stackpot/stackpot/repository/TodoRepository.java b/src/main/java/stackpot/stackpot/repository/TodoRepository.java new file mode 100644 index 00000000..9cf78b48 --- /dev/null +++ b/src/main/java/stackpot/stackpot/repository/TodoRepository.java @@ -0,0 +1,11 @@ +package stackpot.stackpot.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import stackpot.stackpot.domain.mapping.UserTodo; + +import java.time.LocalDateTime; + +public interface TodoRepository extends JpaRepository { + // 하루 전 createdAt 기준으로 데이터 삭제 + int deleteByCreatedAtBefore(LocalDateTime date); +} diff --git a/src/main/java/stackpot/stackpot/repository/UserRepository/UserRepository.java b/src/main/java/stackpot/stackpot/repository/UserRepository/UserRepository.java new file mode 100644 index 00000000..bd3de0c7 --- /dev/null +++ b/src/main/java/stackpot/stackpot/repository/UserRepository/UserRepository.java @@ -0,0 +1,10 @@ +package stackpot.stackpot.repository.UserRepository; + +import org.springframework.data.jpa.repository.JpaRepository; +import stackpot.stackpot.domain.User; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/stackpot/stackpot/service/CustomUserDetailService.java b/src/main/java/stackpot/stackpot/service/CustomUserDetailService.java new file mode 100644 index 00000000..15e0a6e0 --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/CustomUserDetailService.java @@ -0,0 +1,25 @@ +package stackpot.stackpot.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.repository.UserRepository.UserRepository; + +@RequiredArgsConstructor +@Service +public class CustomUserDetailService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + + // UserDetails 반환 + return userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")); + } +} diff --git a/src/main/java/stackpot/stackpot/service/EmailService/EmailService.java b/src/main/java/stackpot/stackpot/service/EmailService/EmailService.java new file mode 100644 index 00000000..dc431c7b --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/EmailService/EmailService.java @@ -0,0 +1,5 @@ +package stackpot.stackpot.service.EmailService; + +public interface EmailService { + void sendSupportNotification(String toEmail, String potName, String applicantName, String applicantIntroduction); +} diff --git a/src/main/java/stackpot/stackpot/service/EmailService/EmailServiceImpl.java b/src/main/java/stackpot/stackpot/service/EmailService/EmailServiceImpl.java new file mode 100644 index 00000000..fef658b0 --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/EmailService/EmailServiceImpl.java @@ -0,0 +1,47 @@ +package stackpot.stackpot.service.EmailService; + +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +public class EmailServiceImpl implements EmailService { + + private final JavaMailSender mailSender; + + public EmailServiceImpl(JavaMailSender mailSender) { + this.mailSender = mailSender; + } + + @Override + public void sendSupportNotification(String toEmail, String potName, String applicantName, String applicantIntroduction) { + try { + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo(toEmail); + message.setSubject("[STACKPOT] 새로운 지원자가 있습니다 - '" + potName + "'"); + + // 이메일 본문 작성 + // 이메일 본문 작성 + String emailBody = String.format( + "[%s]에 새로운 지원자가 있습니다!\n\n" + + "안녕하세요, STACKPOT에서 알려드립니다.\n\n" + + "회원님이 생성하신 [%s]에 새로운 지원자가 지원했습니다. 아래는 지원자 정보와 관련된 세부 사항입니다:\n\n" + + "- 지원자 이름: %s\n" + + "- 한 줄 소개: %s\n\n" + + "STACKPOT과 함께 성공적인 프로젝트를 만들어가세요!\n\n" + + "감사합니다.\n\n" + + "STACKPOT 드림\n\n" + + "고객센터: stackpot.notice@gmail.com\n" + + "홈페이지: https://www.stackpot.co.kr", + potName, potName, applicantName, applicantIntroduction != null ? applicantIntroduction : "없음" + ); + + message.setText(emailBody); + mailSender.send(message); + } catch (Exception e) { + // 예외 처리: 이메일 전송 실패 시 로그를 출력 + System.err.println("이메일 전송 실패: " + e.getMessage()); + } + } + +} diff --git a/src/main/java/stackpot/stackpot/service/FeedService.java b/src/main/java/stackpot/stackpot/service/FeedService.java new file mode 100644 index 00000000..da1944e7 --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/FeedService.java @@ -0,0 +1,20 @@ +package stackpot.stackpot.service; + +import stackpot.stackpot.domain.Feed; +import stackpot.stackpot.domain.enums.Category; +import stackpot.stackpot.web.dto.FeedRequestDto; +import stackpot.stackpot.web.dto.FeedResponseDto; + +public interface FeedService { + public FeedResponseDto.FeedPreviewList getPreViewFeeds(Category category, String sort, String cursor, int limit); + public Feed createFeed(FeedRequestDto.createDto request); + + public Feed getFeed(Long feedId); + + public Feed modifyFeed(long feedId, FeedRequestDto.createDto request); + public boolean toggleLike(Long feedId); + public boolean toggleSave(Long feedId); + + public Long getSaveCount(Long feedId); + public Long getLikeCount(Long feedId); +} diff --git a/src/main/java/stackpot/stackpot/service/FeedServiceImpl.java b/src/main/java/stackpot/stackpot/service/FeedServiceImpl.java new file mode 100644 index 00000000..8a9e1896 --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/FeedServiceImpl.java @@ -0,0 +1,186 @@ +package stackpot.stackpot.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import stackpot.stackpot.converter.FeedConverter; +import stackpot.stackpot.domain.Feed; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.enums.Category; +import stackpot.stackpot.domain.mapping.FeedLike; +import stackpot.stackpot.domain.mapping.FeedSave; +import stackpot.stackpot.repository.FeedLikeRepository; +import stackpot.stackpot.repository.FeedRepository.FeedRepository; +import stackpot.stackpot.repository.FeedSaveRepository; +import stackpot.stackpot.repository.UserRepository.UserRepository; +import stackpot.stackpot.web.dto.FeedRequestDto; +import stackpot.stackpot.web.dto.FeedResponseDto; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +@Service +@RequiredArgsConstructor +public class FeedServiceImpl implements FeedService { + + private final FeedRepository feedRepository; + private final FeedConverter feedConverter; + private final UserRepository userRepository; + private final FeedLikeRepository feedLikeRepository; + private final FeedSaveRepository feedSaveRepository; + + @Override + public FeedResponseDto.FeedPreviewList getPreViewFeeds(Category categor, String sort, String cursor, int limit) { + // 커서가 없으면 현재 시간 사용 + LocalDateTime lastCreatedAt = cursor != null + ? LocalDateTime.parse(cursor) + : LocalDateTime.now(); + + // Pageable 생성 + Pageable pageable = PageRequest.of(0, limit); + + // 데이터 조회 + List feedResults = feedRepository.findFeeds(categor, sort, lastCreatedAt, pageable); + + // Feed와 인기 점수를 DTO로 변환 + List feedDtoList = feedResults.stream() + .map(result -> { + Feed feed = (Feed) result[0]; + int popularity = (int) result[1]; + int likeCount = (int) result[2]; + + return feedConverter.feedDto(feed, popularity, likeCount); + }) + .collect(Collectors.toList()); + + // 다음 커서 계산 + String nextCursor = feedResults.isEmpty() + ? null + : ((Feed) feedResults.get(feedResults.size() - 1)[0]).getCreatedAt().toString(); + + return new FeedResponseDto.FeedPreviewList(feedDtoList, nextCursor); + } + + @Override + public Feed createFeed(FeedRequestDto.createDto request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + Feed feed = feedConverter.toFeed(request); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + + feed.setUser(user); + return feedRepository.save(feed); + + } + + @Override + public Feed getFeed(Long feedId) { + Feed feed = feedRepository.findById(feedId) + .orElseThrow(()->new IllegalArgumentException("해당 피드를 찾을 수 없습니다.")); + return feed; + } + + @Override + public Feed modifyFeed(long feedId, FeedRequestDto.createDto request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + Feed feed = feedRepository.findById(feedId) + .orElseThrow(() -> new IllegalArgumentException("해당 피드를 찾을 수 없습니다.")); + + if(!feed.getUser().getEmail().equals(email)){ + throw new SecurityException("해당 피드를 수정할 권한이 없습니다."); + } + + if(request.getTitle() != null){ + feed.setTitle(request.getTitle()); + } + if(request.getContent() != null){ + feed.setContent(request.getContent()); + } + if(request.getVisibility() != null){ + feed.setVisibility(request.getVisibility()); + } + if(request.getCategor() != null){ + feed.setCategory(request.getCategor()); + } + return feedRepository.save(feed); + } + + @Override + public boolean toggleLike(Long feedId) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String userEmail = authentication.getName(); + + Feed feed = feedRepository.findById(feedId) + .orElseThrow(() -> new IllegalArgumentException("게시물을 찾을 수 없습니다.")); + + User user = userRepository.findByEmail(userEmail) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + + Optional existingLike = feedLikeRepository.findByFeedAndUser(feed, user); + + if (existingLike.isPresent()) { + // 이미 좋아요가 있다면 삭제 (좋아요 취소) + feedLikeRepository.delete(existingLike.get()); + return false; // 좋아요 취소 + } else { + // 좋아요 추가 + FeedLike feedLike = FeedLike.builder() + .feed(feed) + .user(user) + .build(); + + feedLikeRepository.save(feedLike); + return true; // 좋아요 성공 + } + } + + @Override + public boolean toggleSave(Long feedId) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String userEmail = authentication.getName(); + + Feed feed = feedRepository.findById(feedId) + .orElseThrow(() -> new IllegalArgumentException("게시물을 찾을 수 없습니다.")); + + User user = userRepository.findByEmail(userEmail) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + + Optional existingSave = feedSaveRepository.findByFeedAndUser(feed, user); + + if (existingSave.isPresent()) { + feedSaveRepository.delete(existingSave.get()); + return false; + } else { + FeedSave feedSave = FeedSave.builder() + .feed(feed) + .user(user) + .build(); + + feedSaveRepository.save(feedSave); + return true; // 좋아요 성공 + } + } + + @Override + public Long getSaveCount(Long feedId) { + Feed feed = feedRepository.findById(feedId) + .orElseThrow(() -> new IllegalArgumentException("게시물을 찾을 수 없습니다.")); + return feedSaveRepository.countByFeed(feed); + } + + @Override + public Long getLikeCount(Long feedId) { + Feed feed = feedRepository.findById(feedId) + .orElseThrow(() -> new IllegalArgumentException("게시물을 찾을 수 없습니다.")); + return feedLikeRepository.countByFeed(feed); + } +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/service/KakaoService.java b/src/main/java/stackpot/stackpot/service/KakaoService.java new file mode 100644 index 00000000..1eb43805 --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/KakaoService.java @@ -0,0 +1,84 @@ +package stackpot.stackpot.service; + +import io.netty.handler.codec.http.HttpHeaderValues; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import stackpot.stackpot.web.dto.KakaoTokenResponseDto; +import stackpot.stackpot.web.dto.KakaoUserInfoResponseDto; +import org.springframework.http.HttpStatusCode; + +@Slf4j +@RequiredArgsConstructor +@Service +public class KakaoService { + + private String clientId; + private final String KAUTH_TOKEN_URL_HOST; + private final String KAUTH_USER_URL_HOST; + + @Autowired + public KakaoService(@Value("${KAKAO_CLIENT_ID}") String clientId) { + this.clientId = clientId; + this.KAUTH_TOKEN_URL_HOST = "https://kauth.kakao.com"; + this.KAUTH_USER_URL_HOST = "https://kapi.kakao.com"; + } + + public String getAccessTokenFromKakao(String code) { + + KakaoTokenResponseDto kakaoTokenResponseDto = WebClient.create(KAUTH_TOKEN_URL_HOST).post() + .uri(uriBuilder -> uriBuilder + .path("/oauth/token") + .queryParam("grant_type", "authorization_code") + .queryParam("client_id", clientId) + .queryParam("code", code) + .queryParam("scope", "account_email") + .build(true)) + .header(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString()) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> Mono.error(new RuntimeException("Invalid Parameter"))) + .onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new RuntimeException("Internal Server Error"))) + .bodyToMono(KakaoTokenResponseDto.class) + .block(); + + log.info("[Kakao Service] Access Token ------> {}", kakaoTokenResponseDto.getAccessToken()); + log.info("[Kakao Service] Refresh Token ------> {}", kakaoTokenResponseDto.getRefreshToken()); + log.info("[Kakao Service] Id Token ------> {}", kakaoTokenResponseDto.getIdToken()); + log.info("[Kakao Service] Scope ------> {}", kakaoTokenResponseDto.getScope()); + + return kakaoTokenResponseDto.getAccessToken(); + } + + public KakaoUserInfoResponseDto getUserInfo(String accessToken) { + + KakaoUserInfoResponseDto userInfo = WebClient.create(KAUTH_USER_URL_HOST) + .get() + .uri(uriBuilder -> uriBuilder + .path("/v2/user/me") + .build(true)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .header(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString()) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> Mono.error(new RuntimeException("Invalid Parameter"))) + .onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new RuntimeException("Internal Server Error"))) + .bodyToMono(KakaoUserInfoResponseDto.class) + .block(); + + log.info("[Kakao Service] Raw User Info Response: {}", userInfo); + + if (userInfo == null || userInfo.getKakaoAccount() == null) { + log.error("[Kakao Service] Invalid user info response from Kakao API"); + throw new RuntimeException("Invalid user info response from Kakao API"); + } + + log.info("[Kakao Service] Auth ID ------> {}", userInfo.getId()); + log.info("[Kakao Service] email ------> {}", userInfo.getKakaoAccount().getEmail()); + + return userInfo; + } +} diff --git a/src/main/java/stackpot/stackpot/service/MyPotService.java b/src/main/java/stackpot/stackpot/service/MyPotService.java new file mode 100644 index 00000000..2e6ce480 --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/MyPotService.java @@ -0,0 +1,30 @@ +package stackpot.stackpot.service; + +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.Taskboard; +import stackpot.stackpot.domain.mapping.UserTodo; +import stackpot.stackpot.web.dto.*; + +import java.util.List; +import java.util.Map; + +public interface MyPotService { + + // 사용자의 진행 중인 팟 조회 + Map> getMyOnGoingPots(); + + + // 사용자의 특정 팟에서의 생성 + List postTodo(Long potId, MyPotTodoRequestDTO requestDTO); + + List getTodo(Long potId); + + List updateTodos(Long potId, List requestList); + + List completeTodo(Long potId, Long todoId); + + MyPotTaskResponseDto creatTask(Long potId, MyPotTaskRequestDto.create request); + MyPotTaskResponseDto viewDetailTask(Long taskId); + MyPotTaskResponseDto modfiyTask(Long taskId, MyPotTaskRequestDto.create request); + void deleteTaskboard(Long potId, Long taskboardId); +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/service/MyPotServiceImpl.java b/src/main/java/stackpot/stackpot/service/MyPotServiceImpl.java new file mode 100644 index 00000000..de776329 --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/MyPotServiceImpl.java @@ -0,0 +1,449 @@ +package stackpot.stackpot.service; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import stackpot.stackpot.apiPayload.code.status.ErrorStatus; +import stackpot.stackpot.apiPayload.exception.handler.MemberHandler; +import stackpot.stackpot.apiPayload.exception.handler.PotHandler; +import stackpot.stackpot.converter.PotConverter; +import stackpot.stackpot.converter.TaskboardConverter; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.Taskboard; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.enums.TodoStatus; +import stackpot.stackpot.domain.mapping.PotMember; +import stackpot.stackpot.domain.mapping.Task; +import stackpot.stackpot.domain.mapping.UserTodo; +import stackpot.stackpot.repository.PotMemberRepository; +import stackpot.stackpot.repository.PotRepository.MyPotRepository; +import stackpot.stackpot.repository.PotRepository.PotRepository; +import stackpot.stackpot.repository.TaskRepository; +import stackpot.stackpot.repository.TaskboardRepository; +import stackpot.stackpot.repository.UserRepository.UserRepository; +import stackpot.stackpot.web.dto.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class MyPotServiceImpl implements MyPotService { + + private final PotRepository potRepository; + private final MyPotRepository myPotRepository; + private final UserRepository userRepository; + private final PotConverter potConverter; + private final TaskboardConverter taskboardConverter; + private final TaskboardRepository taskboardRepository; + private final PotMemberRepository potMemberRepository; + private final TaskRepository taskRepository; + + + @Override + public Map> getMyOnGoingPots() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new IllegalArgumentException("User not found with email: " + email)); + + // 1. 내가 PotMember로 참여 중이고 상태가 'ONGOING'인 팟 조회 + List ongoingMemberPots = potRepository.findByPotMembers_UserIdAndPotStatus(user.getId(), "ONGOING"); + + // 2. 내가 만든 팟 중 상태가 'ONGOING'인 팟 조회 + List ongoingOwnedPots = potRepository.findByUserIdAndPotStatus(user.getId(), "ONGOING"); + + // Pot 리스트를 DTO로 변환 + List memberPotsDetails = ongoingMemberPots.stream() + .map(this::convertToOngoingPotDetail) + .collect(Collectors.toList()); + + List ownedPotsDetails = ongoingOwnedPots.stream() + .map(this::convertToOngoingPotDetail) + .collect(Collectors.toList()); + + // 결과를 분류하여 반환 + Map> result = new HashMap<>(); + result.put("joinedOngoingPots", memberPotsDetails); + result.put("ownedOngoingPots", ownedPotsDetails); + + return result; + } + + + @Override + public List postTodo(Long potId, MyPotTodoRequestDTO requestDTO) { + // 현재 인증된 사용자 가져오기 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + // 사용자 정보 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + // 팟 조회 + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND)); + + // 소유자 확인 + if (!pot.getUser().equals(user)) { + throw new PotHandler(ErrorStatus.POT_FORBIDDEN); + } + + // To-Do 생성 + UserTodo userTodo = UserTodo.builder() + .pot(pot) + .user(user) + .content(requestDTO.getContent()) + .status(requestDTO.getStatus()) + .build(); + + myPotRepository.save(userTodo); + + // 특정 팟의 모든 To-Do 조회 (업데이트된 리스트) + List potTodos = myPotRepository.findByPot_PotId(potId); + + // 사용자별로 그룹화하여 반환 + return potTodos.stream() + .collect(Collectors.groupingBy(UserTodo::getUser)) + .entrySet().stream() + .map(entry -> { + // 해당 유저의 pot에서 potMember 역할 찾기 + String roleName = getUserRoleInPot(entry.getKey(), pot); + + return MyPotTodoResponseDTO.builder() + .userNickname(entry.getKey().getNickname() + getVegetableNameByRole(roleName)) + .userId(entry.getKey().getId()) + .todos(entry.getValue().stream() + .map(todo -> MyPotTodoResponseDTO.TodoDetailDTO.builder() + .todoId(todo.getTodoId()) + .content(todo.getContent()) + .status(todo.getStatus()) + .build()) + .collect(Collectors.toList())) + .build(); + }) + .collect(Collectors.toList()); + } + + @Override + public List getTodo(Long potId) { + // 현재 인증된 사용자 가져오기 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + // 사용자 정보 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + // 팟 조회 + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND)); + + // 소유자 확인 + if (!pot.getUser().equals(user)) { + throw new PotHandler(ErrorStatus.POT_FORBIDDEN); + } + + // 특정 팟의 모든 To-Do 조회 + List potTodos = myPotRepository.findByPot_PotId(potId); + + // 특정 팟의 모든 To-Do 조회 + return potTodos.stream() + .collect(Collectors.groupingBy(UserTodo::getUser)) + .entrySet().stream() + .map(entry -> { + // 해당 유저의 pot에서 potMember 역할 찾기 + String roleName = getUserRoleInPot(entry.getKey(), pot); // 기본값을 String으로 설정 + + return MyPotTodoResponseDTO.builder() + .userNickname(entry.getKey().getNickname() + getVegetableNameByRole(roleName)) + .userId(entry.getKey().getId()) + .todos(entry.getValue().stream() + .map(todo -> MyPotTodoResponseDTO.TodoDetailDTO.builder() + .todoId(todo.getTodoId()) + .content(todo.getContent()) + .status(todo.getStatus()) + .build()) + .collect(Collectors.toList())) + .build(); + }) + .collect(Collectors.toList()); + } + + @Override + @Transactional + public List updateTodos(Long potId, List requestList) { + // 현재 인증된 사용자 가져오기 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + // 사용자 정보 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + // 팟 조회 + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND)); + + // 소유자 확인 + if (!pot.getUser().equals(user)) { + throw new PotHandler(ErrorStatus.POT_FORBIDDEN); + } + + // 특정 팟에 속한 모든 투두 리스트 조회 (사용자별) + List userTodos = myPotRepository.findByPotAndUser(pot, user); + + // 요청된 todoId와 일치하는 항목 업데이트 + Map todoMap = userTodos.stream() + .collect(Collectors.toMap(UserTodo::getTodoId, todo -> todo)); + + for (MyPotTodoUpdateRequestDTO updateRequest : requestList) { + UserTodo todo = todoMap.get(updateRequest.getTodoId()); + if (todo == null) { + throw new IllegalArgumentException("Todo with ID " + updateRequest.getTodoId() + " not found."); + } + + // 내용 업데이트 + todo.setContent(updateRequest.getContent()); + } + + // 변경된 상태 저장 + myPotRepository.saveAll(userTodos); + + // 사용자별로 그룹화하여 DTO로 변환 + Map> groupedByUser = userTodos.stream() + .collect(Collectors.groupingBy(UserTodo::getUser)); + + return groupedByUser.entrySet().stream() + .map(entry -> { + // 해당 유저의 pot에서 potMember 역할 찾기 + String roleName = getUserRoleInPot(entry.getKey(), pot); + + return MyPotTodoResponseDTO.builder() + .userNickname(entry.getKey().getNickname() + getVegetableNameByRole(roleName)) + .userId(entry.getKey().getId()) + .todos(entry.getValue().stream() + .map(todo -> MyPotTodoResponseDTO.TodoDetailDTO.builder() + .todoId(todo.getTodoId()) + .content(todo.getContent()) + .status(todo.getStatus()) + .build()) + .collect(Collectors.toList())) + .build(); + }) + .collect(Collectors.toList()); + } + + @Transactional + @Override + public List completeTodo(Long potId, Long todoId) { + // 현재 로그인한 사용자 확인 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + // 사용자 정보 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + // 팟 조회 + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND)); + + // 해당 투두가 존재하는지 확인 및 소유자 검증 + UserTodo userTodo = myPotRepository.findByTodoIdAndPot_PotId(todoId, potId) + .orElseThrow(() -> new PotHandler(ErrorStatus.USER_TODO_NOT_FOUND)); + + if (!userTodo.getUser().equals(user)) { + throw new PotHandler(ErrorStatus.USER_TODO_UNAUTHORIZED); + } + + // To-Do 상태 업데이트 + userTodo.setStatus(TodoStatus.COMPLETED); + myPotRepository.save(userTodo); + + // 특정 팟의 모든 To-Do 조회 후 반환 + List potTodos = myPotRepository.findByPot_PotId(potId); + + return potTodos.stream() + .collect(Collectors.groupingBy(UserTodo::getUser)) + .entrySet().stream() + .map(entry -> { + // 소유자인지 확인하고 적절한 역할 적용 + String roleName = getUserRoleInPot(entry.getKey(), pot); + String userNicknameWithRole = entry.getKey().getNickname() + getVegetableNameByRole(roleName); + + return MyPotTodoResponseDTO.builder() + .userNickname(userNicknameWithRole) + .userId(entry.getKey().getId()) + .todos(entry.getValue().stream() + .map(todo -> MyPotTodoResponseDTO.TodoDetailDTO.builder() + .todoId(todo.getTodoId()) + .content(todo.getContent()) + .status(todo.getStatus()) + .build()) + .collect(Collectors.toList())) + .build(); + }) + .collect(Collectors.toList()); + } + + + @Override + public MyPotTaskResponseDto creatTask(Long potId, MyPotTaskRequestDto.create request) { + + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new IllegalArgumentException("Pot not found with id: " + potId)); + + Taskboard taskboard = taskboardConverter.toTaskboard(pot, request); + taskboardRepository.save(taskboard); + + List participants = potMemberRepository.findAllById(request.getParticipants()); + + log.info("dd", participants); + + if (participants.isEmpty()) { + throw new IllegalArgumentException("유효한 참가자를 찾을 수 없습니다. 요청된 ID를 확인해주세요."); + } + createAndSaveTasks(taskboard, participants); + List participantDtos = taskboardConverter.toParticipantDtoList(participants); + + MyPotTaskResponseDto response = taskboardConverter.toDTO(taskboard); + response.setParticipants(participantDtos); + return response; + } + + @Override + public MyPotTaskResponseDto viewDetailTask(Long taskboardId) { + + Taskboard taskboard = taskboardRepository.findById(taskboardId) + .orElseThrow(() -> new IllegalArgumentException("Taskboard not found with id: " + taskboardId)); + + MyPotTaskResponseDto response = taskboardConverter.toDTO(taskboard); + + return response; + } + + private MyPotResponseDTO.OngoingPotsDetail convertToOngoingPotDetail(Pot pot) { + List potMembers = pot.getPotMembers().stream() + .map(member -> PotMemberResponseDTO.builder() + .potMemberId(member.getPotMemberId()) + .roleName(member.getRoleName()) + .appealContent(member.getAppealContent()) + .build()) + .collect(Collectors.toList()); + + return MyPotResponseDTO.OngoingPotsDetail.builder() + .user(UserResponseDto.builder() + .nickname(pot.getUser().getNickname()) + .role(pot.getUser().getRole()) + .build()) + .pot(potConverter.toDto(pot, pot.getRecruitmentDetails())) + .potMembers(potMembers) + .build(); + } + + + @Override + public MyPotTaskResponseDto modfiyTask(Long taskId, MyPotTaskRequestDto.create request) { + + Taskboard taskboard = taskboardRepository.findById(taskId) + .orElseThrow(() -> new IllegalArgumentException("Taskboard not found with id: " + taskId)); + + updateUserData(taskboard, request); + + List participants = new ArrayList<>(); + if (request.getParticipants() != null && !request.getParticipants().isEmpty()) { + participants = potMemberRepository.findAllById(request.getParticipants()); + if (participants.isEmpty()) { + throw new IllegalArgumentException("유효한 참가자를 찾을 수 없습니다. 요청된 ID를 확인해주세요."); + } + } + createAndSaveTasks(taskboard, participants); + List participantDtos = taskboardConverter.toParticipantDtoList(participants); + + MyPotTaskResponseDto response = taskboardConverter.toDTO(taskboard); + response.setParticipants(participantDtos); + + return response; + } + + @Transactional + @Override + public void deleteTaskboard(Long potId, Long taskboardId) { + Taskboard taskboard = taskboardRepository.findById(taskboardId) + .orElseThrow(() -> new IllegalArgumentException("Taskboard not found with id: " + taskboardId)); + +// // Taskboard가 해당 Pot에 속해 있는지 확인 +// if (!taskboard.getPot().getId().equals(potId)) { +// throw new IllegalArgumentException("The taskboard does not belong to the specified pot."); +// } + + // Taskboard에 연결된 Task 삭제 + List tasks = taskRepository.findByTaskboard(taskboard); + taskRepository.deleteAll(tasks); + + // Taskboard 삭제 + taskboardRepository.delete(taskboard); + } + + // 역할에 따른 채소명을 반환하는 메서드 + private String getVegetableNameByRole(String role) { + Map roleToVegetableMap = Map.of( + "BACKEND", " 양파", + "FRONTEND", " 버섯", + "DESIGN", " 브로콜리", + "PLANNING", " 당근" + ); + return roleToVegetableMap.getOrDefault(role, "알 수 없음"); + } + + + private String getUserRoleInPot(User user, Pot pot) { + if (pot.getUser().equals(user)) { + // 소유자인 경우, 사용자의 역할을 직접 가져옴 + return pot.getUser().getRole().name(); + } else { + // 참여자인 경우, potMember에서 역할을 가져옴 + return pot.getPotMembers().stream() + .filter(member -> member.getUser().equals(user)) + .map(member -> member.getRoleName().name()) // ENUM -> String 변환 + .findFirst() + .orElse("UNKNOWN"); // 기본값 설정 + } + } + + private void updateUserData(Taskboard taskboard, MyPotTaskRequestDto.create request) { + if(request.getTitle() !=null){ + taskboard.setTitle(request.getTitle()); + } + if(request.getDescription()!=null){ + taskboard.setDescription(request.getDescription()); + } + if(request.getDeadline()!=null){ + taskboard.setEndDate(request.getDeadline()); + } + if(request.getTaskboardStatus()!=null){ + taskboard.setStatus(request.getTaskboardStatus()); + } + } + + private List createAndSaveTasks(Taskboard taskboard, List participants) { + List tasks = participants.stream() + .map(participant -> Task.builder() + .taskboard(taskboard) + .potMember(participant) + .build()) + .collect(Collectors.toList()); + + return taskRepository.saveAll(tasks); + } +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/service/PotApplicationService/PotApplicationService.java b/src/main/java/stackpot/stackpot/service/PotApplicationService/PotApplicationService.java new file mode 100644 index 00000000..79c9d48b --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/PotApplicationService/PotApplicationService.java @@ -0,0 +1,12 @@ +package stackpot.stackpot.service.PotApplicationService; + +import stackpot.stackpot.web.dto.PotApplicationRequestDto; +import stackpot.stackpot.web.dto.PotApplicationResponseDto; + +import java.util.List; + +public interface PotApplicationService { + PotApplicationResponseDto applyToPot(PotApplicationRequestDto dto,Long potId); + + List getApplicantsByPotId(Long potId); +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/service/PotApplicationService/PotApplicationServiceImpl.java b/src/main/java/stackpot/stackpot/service/PotApplicationService/PotApplicationServiceImpl.java new file mode 100644 index 00000000..5ab5e4f0 --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/PotApplicationService/PotApplicationServiceImpl.java @@ -0,0 +1,108 @@ +package stackpot.stackpot.service.PotApplicationService; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.transaction.annotation.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import stackpot.stackpot.converter.PotApplicationConverter.PotApplicationConverter; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.enums.ApplicationStatus; +import stackpot.stackpot.domain.mapping.PotApplication; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.repository.PotApplicationRepository.PotApplicationRepository; +import stackpot.stackpot.repository.PotRepository.PotRepository; +import stackpot.stackpot.repository.UserRepository.UserRepository; +import stackpot.stackpot.config.security.JwtTokenProvider; +import stackpot.stackpot.service.EmailService.EmailService; +import stackpot.stackpot.web.dto.PotApplicationRequestDto; +import stackpot.stackpot.web.dto.PotApplicationResponseDto; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PotApplicationServiceImpl implements PotApplicationService { + + private final PotApplicationRepository potApplicationRepository; + private final PotRepository potRepository; + private final UserRepository userRepository; + private final PotApplicationConverter potApplicationConverter; + private final JwtTokenProvider jwtTokenProvider; + private final EmailService emailService; + @Transactional + public PotApplicationResponseDto applyToPot(PotApplicationRequestDto dto, Long potId) { + // 인증된 사용자 이메일 가져오기 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + // 사용자 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + + // 팟 조회 + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new IllegalArgumentException("해당 팟을 찾을 수 없습니다.")); + + // 중복 지원 방지 + if (potApplicationRepository.existsByUserIdAndPot_PotId(user.getId(), potId)) { + throw new IllegalStateException("이미 해당 팟에 지원하셨습니다."); + } + + // 지원 엔티티 생성 및 저장 + PotApplication potApplication = potApplicationConverter.toEntity(dto, pot, user); + potApplication.setApplicationStatus(ApplicationStatus.PENDING); + potApplication.setAppliedAt(LocalDateTime.now()); + + PotApplication savedApplication = potApplicationRepository.save(potApplication); + + // 이메일 전송 + // 이메일 전송 + emailService.sendSupportNotification( + pot.getUser().getEmail(), + pot.getPotName(), + user.getNickname(), + user.getUserIntroduction() // 한 줄 소개 추가 + ); + + // 저장된 지원 정보를 응답 DTO로 변환 + return potApplicationConverter.toDto(savedApplication); + } + + + + @Override + @Transactional(readOnly = true) + public List getApplicantsByPotId(Long potId) { + // 현재 인증된 사용자 이메일 가져오기 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + // 사용자 정보 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new IllegalArgumentException("User not found with email: " + email)); + // 팟 조회 + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new IllegalArgumentException("해당 팟을 찾을 수 없습니다.")); + + + + // 소유자 확인 + if (!pot.getUser().equals(user)) { + throw new IllegalArgumentException("해당 팟 지원자 목록을 볼 수 있는 권한이 없습니다."); + } + + // 지원자 목록 조회 + List applications = potApplicationRepository.findByPot_PotId(potId); + + // DTO 변환 후 반환 + return applications.stream() + .map(potApplicationConverter::toDto) + .collect(Collectors.toList()); + } + + + +} diff --git a/src/main/java/stackpot/stackpot/service/PotMemberService/PotMemberService.java b/src/main/java/stackpot/stackpot/service/PotMemberService/PotMemberService.java new file mode 100644 index 00000000..1db299a3 --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/PotMemberService/PotMemberService.java @@ -0,0 +1,13 @@ +package stackpot.stackpot.service.PotMemberService; + +import stackpot.stackpot.web.dto.PotMemberRequestDto; +import stackpot.stackpot.web.dto.PotMemberAppealResponseDto; + +import java.util.List; + +public interface PotMemberService { + List getPotMembers(Long potId); + List addMembersToPot(Long potId, PotMemberRequestDto requestDto); + void updateAppealContent(Long potId, Long memberId, String appealContent); + void validateIsOwner(Long potId); // 팟 생성자 검증 +} diff --git a/src/main/java/stackpot/stackpot/service/PotMemberService/PotMemberServiceImpl.java b/src/main/java/stackpot/stackpot/service/PotMemberService/PotMemberServiceImpl.java new file mode 100644 index 00000000..a5985131 --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/PotMemberService/PotMemberServiceImpl.java @@ -0,0 +1,111 @@ +package stackpot.stackpot.service.PotMemberService; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import stackpot.stackpot.converter.PotMemberConverter.PotMemberConverter; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.mapping.PotApplication; +import stackpot.stackpot.domain.mapping.PotMember; +import stackpot.stackpot.repository.PotApplicationRepository.PotApplicationRepository; +import stackpot.stackpot.repository.PotMemberRepository; +import stackpot.stackpot.repository.PotRepository.PotRepository; +import stackpot.stackpot.repository.UserRepository.UserRepository; +import stackpot.stackpot.web.dto.PotMemberRequestDto; +import stackpot.stackpot.web.dto.PotMemberAppealResponseDto; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PotMemberServiceImpl implements PotMemberService { + + private final PotRepository potRepository; + private final UserRepository userRepository; + private final PotApplicationRepository potApplicationRepository; + private final PotMemberRepository potMemberRepository; + private final PotMemberConverter potMemberConverter; + + @Transactional + @Override + public List getPotMembers(Long potId) { + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new IllegalArgumentException("해당 팟을 찾을 수 없습니다.")); + + List potMembers = potMemberRepository.findByPotId(potId); + return potMembers.stream() + .map(potMemberConverter::toDto) + .collect(Collectors.toList()); + } + @Transactional + @Override + public List addMembersToPot (Long potId, PotMemberRequestDto requestDto) { + // 1. 팟 조회 + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new IllegalArgumentException("해당 팟을 찾을 수 없습니다.")); + + // 2. 팟 상태를 "ing"로 설정 + pot.setPotStatus("ONGOING"); + + // 3. 팟의 시작 날짜를 현재 날짜로 설정 + pot.setPotStartDate(LocalDate.now()); // 필드 이름에 따라 메서드 호출 + potRepository.save(pot); // 변경 사항 저장 + + + // 4. 선택된 지원자들을 멤버로 추가 + List applicantIds = requestDto.getApplicantIds(); + List newMembers = new ArrayList<>(); + + for (Long applicantId : applicantIds) { + + PotApplication application = potApplicationRepository.findById(applicantId) + .orElseThrow(() -> new IllegalArgumentException("지원자를 찾을 수 없습니다.")); + User user = application.getUser(); + PotMember member = potMemberConverter.toEntity(user, pot, application, false); + newMembers.add(member); + } + + // 5. 저장 및 응답 반환 + List savedMembers = potMemberRepository.saveAll(newMembers); + return savedMembers.stream() + .map(potMemberConverter::toDto) + .collect(Collectors.toList()); + } + @Transactional + @Override + public void updateAppealContent(Long potId, Long memberId, String appealContent) { + // 1. 멤버 조회 + PotMember potMember = potMemberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("해당 멤버를 찾을 수 없습니다.")); + + // 2. 멤버가 해당 팟에 속해 있는지 확인 + if (!potMember.getPot().getPotId().equals(potId)) { + throw new IllegalArgumentException("해당 멤버는 지정된 팟에 속해 있지 않습니다."); + } + + // 3. 어필 내용 업데이트 + potMember.setAppealContent(appealContent); + potMemberRepository.save(potMember); // 변경 사항 저장 + } + @Override + public void validateIsOwner(Long potId) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new IllegalArgumentException("해당 팟을 찾을 수 없습니다.")); + + if (!pot.getUser().getEmail().equals(email)) { + throw new AccessDeniedException("해당 작업은 팟 생성자만 수행할 수 있습니다."); + } + } + + +} diff --git a/src/main/java/stackpot/stackpot/service/PotService.java b/src/main/java/stackpot/stackpot/service/PotService.java new file mode 100644 index 00000000..bf369eee --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/PotService.java @@ -0,0 +1,41 @@ +package stackpot.stackpot.service; + +import stackpot.stackpot.domain.enums.Role; +import stackpot.stackpot.web.dto.*; + +import java.util.List; + + +public interface PotService { + PotResponseDto createPotWithRecruitments(PotRequestDto requestDto); + PotResponseDto updatePotWithRecruitments(Long potId, PotRequestDto requestDto); + CursorPageResponse getMyCompletedPots(Long cursor, int size); + void deletePot(Long potId); + void removeMemberFromPot(Long potId); + String removePotOrMember(Long potId); + + //--------------- + + // 모집 역할에 따라 모든 팟 조회 + List getAllPots(Role role, Integer page, Integer size); + + // 특정 팟의 세부 정보 조회 + ApplicantResponseDTO getPotDetails(Long potId); + + // 특정 지원자의 좋아요 상태 수정 + void patchLikes(Long potId, Long applicationId, Boolean liked); + + // 특정 팟의 좋아요한 지원자 목록 조회 + List getLikedApplicants(Long potId); + + // 사용자가 지원한 팟 목록 조회 + List getAppliedPots(); + + // 사용자가 참여 중인 팟 목록 조회 + List getMyPots(); + + // 팟 다 끓이기 + void patchPotStatus(Long potId); + + PotSummaryResponseDTO getPotSummary(Long potId); +} diff --git a/src/main/java/stackpot/stackpot/service/PotServiceImpl.java b/src/main/java/stackpot/stackpot/service/PotServiceImpl.java new file mode 100644 index 00000000..10b403b8 --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/PotServiceImpl.java @@ -0,0 +1,483 @@ +// Service +package stackpot.stackpot.service; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import stackpot.stackpot.apiPayload.code.status.ErrorStatus; +import stackpot.stackpot.apiPayload.exception.handler.ApplicationHandler; +import stackpot.stackpot.apiPayload.exception.handler.MemberHandler; +import stackpot.stackpot.apiPayload.exception.handler.PotHandler; +import stackpot.stackpot.config.security.JwtTokenProvider; +import stackpot.stackpot.converter.PotConverter; +import stackpot.stackpot.converter.UserConverter; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.PotRecruitmentDetails; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.enums.Role; +import stackpot.stackpot.domain.mapping.PotApplication; +import stackpot.stackpot.domain.mapping.PotMember; +import stackpot.stackpot.repository.PotMemberRepository; +import stackpot.stackpot.repository.PotRepository.PotRecruitmentDetailsRepository; +import stackpot.stackpot.repository.PotRepository.PotRepository; +import stackpot.stackpot.repository.UserRepository.UserRepository; +import stackpot.stackpot.web.dto.*; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + + +@Service +@RequiredArgsConstructor +public class PotServiceImpl implements PotService { + + private final PotRepository potRepository; + private final PotRecruitmentDetailsRepository recruitmentDetailsRepository; + private final PotConverter potConverter; + private final JwtTokenProvider jwtTokenProvider; + private final UserRepository userRepository; + private final PotMemberRepository potMemberRepository; + @Transactional + public PotResponseDto createPotWithRecruitments(PotRequestDto requestDto) { + // 인증 정보에서 사용자 이메일 가져오기 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + // 사용자 정보 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + // 팟 생성 + Pot pot = potConverter.toEntity(requestDto, user); + // 2. 팟 상태를 "ing"로 설정 + pot.setPotStatus("RECRUITING"); + Pot savedPot = potRepository.save(pot); + + // 모집 정보 저장 + List recruitmentDetails = requestDto.getRecruitmentDetails().stream() + .map(recruitmentDto -> PotRecruitmentDetails.builder() + .recruitmentRole(Role.valueOf(recruitmentDto.getRecruitmentRole())) + .recruitmentCount(recruitmentDto.getRecruitmentCount()) + .pot(savedPot) + .build()) + .collect(Collectors.toList()); + recruitmentDetailsRepository.saveAll(recruitmentDetails); + + // DTO로 변환 후 반환 + return potConverter.toDto(savedPot, recruitmentDetails); + } + + @Transactional + @Override + public PotResponseDto updatePotWithRecruitments(Long potId, PotRequestDto requestDto) { + // 인증 정보에서 사용자 이메일 가져오기 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + // 사용자 정보 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + // 팟 조회 + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND)); + + // 소유자 확인 + if (!pot.getUser().equals(user)) { + throw new PotHandler(ErrorStatus.POT_FORBIDDEN); + } + + // 업데이트 로직 + pot.updateFields(Map.of( + "potName", requestDto.getPotName(), +// "potStartDate", requestDto.getPotStartDate(), + "potEndDate", requestDto.getPotEndDate(), + "potDuration", requestDto.getPotDuration(), + "potLan", requestDto.getPotLan(), + "potContent", requestDto.getPotContent(), + "potStatus", requestDto.getPotStatus(), + "potModeOfOperation", requestDto.getPotModeOfOperation(), + "potSummary", requestDto.getPotSummary(), + "recruitmentDeadline", requestDto.getRecruitmentDeadline() + )); + + // 기존 모집 정보 삭제 + recruitmentDetailsRepository.deleteByPot_PotId(potId); + + // 새로운 모집 정보 저장 + List recruitmentDetails = requestDto.getRecruitmentDetails().stream() + .map(recruitmentDto -> PotRecruitmentDetails.builder() + .recruitmentRole(Role.valueOf(recruitmentDto.getRecruitmentRole())) + .recruitmentCount(recruitmentDto.getRecruitmentCount()) + .pot(pot) + .build()) + .collect(Collectors.toList()); + recruitmentDetailsRepository.saveAll(recruitmentDetails); + + // DTO로 변환 후 반환 + return potConverter.toDto(pot, recruitmentDetails); + } + + @Transactional + @Override + public CursorPageResponse getMyCompletedPots(Long cursor, int size) { + // 현재 인증된 사용자 가져오기 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + // 사용자 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + + // 사용자가 참여하거나 생성한 COMPLETED 상태의 팟 가져오기 + List pots = potRepository.findCompletedPotsByCursor(user.getId(), cursor); + + // 커서 및 데이터 반환 + List result = pots.size() > size ? pots.subList(0, size) : pots; + Long nextCursor = result.isEmpty() ? null : result.get(result.size() - 1).getPotId(); + + List content = result.stream() + .map(pot -> { + List roleCounts = potMemberRepository.findRoleCountsByPotId(pot.getPotId()); + Map roleCountsMap = roleCounts.stream() + .collect(Collectors.toMap( + roleCount -> ((Role) roleCount[0]).name(), + roleCount -> ((Long) roleCount[1]).intValue() + )); + return potConverter.toCompletedPotResponseDto(pot, roleCountsMap); + }) + .collect(Collectors.toList()); + + return new CursorPageResponse<>(content, nextCursor, pots.size() > size); + } + + @Transactional + public void deletePot(Long potId) { + // 인증 정보에서 사용자 이메일 가져오기 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + // 사용자 정보 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + // 팟 조회 + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND)); + + // 팟 소유자 확인 + if (!pot.getUser().equals(user)) { + throw new PotHandler(ErrorStatus.POT_FORBIDDEN); + } + + // 모집 정보 삭제 + recruitmentDetailsRepository.deleteByPot_PotId(potId); + + // 팟 삭제 + potRepository.delete(pot); + } + + +//------------------- + + private final PotSummarizationService potSummarizationService; + + @Transactional + @Override + public List getAllPots(Role role, Integer page, Integer size) { + Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); + Page potPage; + + if (role == null) { + potPage = potRepository.findAll(pageable); + } else { + potPage = potRepository.findByRecruitmentDetails_RecruitmentRole(role, pageable); + } + + return potPage.getContent().stream() + .map(pot -> PotAllResponseDTO.PotDetail.builder() + .user(UserConverter.toDto(pot.getUser())) + .pot(potConverter.toDto(pot, pot.getRecruitmentDetails())) // 변환기 사용 + .build()) + .collect(Collectors.toList()); + } + + + @Override + public ApplicantResponseDTO getPotDetails(Long potId) { + Pot pot = potRepository.findPotWithRecruitmentDetailsByPotId(potId) + .orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND)); + + // 지원자 정보를 DTO로 변환 + List applicantDto = pot.getPotApplication().stream() + .map(app -> ApplicantResponseDTO.ApplicantDto.builder() + .applicationId(app.getApplicationId()) + .potRole(String.valueOf(app.getPotRole())) + .liked(app.getLiked()) + .build()) + .collect(Collectors.toList()); + + + return ApplicantResponseDTO.builder() + .user(UserConverter.toDto(pot.getUser())) + .pot(potConverter.toDto(pot, pot.getRecruitmentDetails())) // 변환기 사용 + .applicant(applicantDto) + .build(); + } + + // 특정 팟 지원자의 좋아요 상태 변경 + @Override + public void patchLikes(Long potId, Long applicationId, Boolean liked) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND)); + + // 팟 생성자 확인 + if (!pot.getUser().getId().equals(user.getId())) { + throw new PotHandler(ErrorStatus.POT_FORBIDDEN); + } + + PotApplication application = pot.getPotApplication().stream() + .filter(app -> app.getApplicationId().equals(applicationId)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Application not found with id: " + applicationId)); + + application.setLiked(liked); + potRepository.save(pot); + } + + // 특정 팟의 좋아요한 지원자 목록 조회 + @Override + public List getLikedApplicants(Long potId) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND)); + + // 팟 생성자 확인 + if (!pot.getUser().getId().equals(user.getId())) { + throw new PotHandler(ErrorStatus.POT_FORBIDDEN); + } + + return pot.getPotApplication().stream() + .filter(PotApplication::getLiked) + .map(app -> LikedApplicantResponseDTO.builder() + .applicationId(app.getApplicationId()) + .applicantRole(app.getPotRole()) + .potNickname(app.getUser().getNickname() + getVegetableNameByRole(String.valueOf(app.getPotRole()))) + .liked(app.getLiked()) + .build()) + .collect(Collectors.toList()); + } + + private String getVegetableNameByRole(String role) { + Map roleToVegetableMap = Map.of( + "DESIGN", " 브로콜리", + "PLANNING", " 당근", + "BACKEND", " 양파", + "FRONTEND", " 버섯" + ); + + return roleToVegetableMap.getOrDefault(role, "알 수 없음"); + } + + + @Override + public List getAppliedPots() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + // 사용자가 지원한 팟 조회 + List appliedPots = potRepository.findByPotApplication_User_Id(user.getId()); + if (appliedPots.isEmpty()) { + throw new ApplicationHandler(ErrorStatus.APPLICATION_NOT_FOUND); + } + + + return appliedPots.stream() + .map(pot -> PotAllResponseDTO.PotDetail.builder() + .user(UserConverter.toDto(pot.getUser())) + .pot(potConverter.toDto(pot, pot.getRecruitmentDetails())) + .build() + ) + .collect(Collectors.toList()); + } + + // 사용자가 만든 팟 조회 + @Override + public List getMyPots() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + // 사용자가 만든 팟 조회 + List myPots = potRepository.findByUserId(user.getId()); + + // 모집중인 팟 리스트 (recruiting 상태 필터링) + List recruitingPots = myPots.stream() + .filter(pot -> "RECRUITING".equalsIgnoreCase(pot.getPotStatus())) // 소문자 비교 + .map(this::convertToPotDetail) + .collect(Collectors.toList()); + + // 진행 중인 팟 리스트 (ongoing 상태 필터링) + List ongoingPots = myPots.stream() + .filter(pot -> "ONGOING".equalsIgnoreCase(pot.getPotStatus())) // 소문자 비교 + .map(this::convertToOngoingPotDetail) + .collect(Collectors.toList()); + + // 끓인 팟 리스트 + List completedPots = myPots.stream() + .filter(pot -> "COMPLETED".equals(pot.getPotStatus())) + .map(this::convertToPotDetail) + .collect(Collectors.toList()); + + return List.of(PotAllResponseDTO.builder() + .recruitingPots(recruitingPots) + .ongoingPots(ongoingPots) + .completedPots(completedPots) + .build()); + + } + + @Override + public void patchPotStatus(Long potId) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND)); + + // 팟 생성자 확인 + if (!pot.getUser().getId().equals(user.getId())) { + throw new PotHandler(ErrorStatus.POT_FORBIDDEN); + } + + // 팟 상태를 "complete"으로 변경 + pot.setPotStatus("COMPLETED"); + + // 변경된 상태 저장 + potRepository.save(pot); + } + + @Override + public PotSummaryResponseDTO getPotSummary(Long potId) { + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND)); + + String prompt = "구인글에 내용을 우리 프로젝트를 소개하는 400자로 정리해줘. " + + "기획 배경, 주요기능, 어떤 언어와 프레임워크 사용했는지 등등 구체적인게 들어있으면 더 좋아.\n" + + "내용: " + pot.getPotContent(); + + String summary = potSummarizationService.summarizeText(prompt, 400); + + return PotSummaryResponseDTO.builder() + .summary(summary) + .build(); + } + + // Pot을 PotAllResponseDTO.PotDetail로 변환하는 메서드 + private PotAllResponseDTO.PotDetail convertToPotDetail(Pot pot) { + + return PotAllResponseDTO.PotDetail.builder() + .user(UserConverter.toDto(pot.getUser())) + .pot(potConverter.toDto(pot, pot.getRecruitmentDetails())) // 변환기 사용 + .build(); + } + + // 진행 중인 팟 변환 메서드 (멤버 포함) + private MyPotResponseDTO.OngoingPotsDetail convertToOngoingPotDetail(Pot pot) { + + List potMembers = pot.getPotMembers().stream() + .map(member -> PotMemberResponseDTO.builder() + .potMemberId(member.getPotMemberId()) + .roleName(member.getRoleName()) + .build()) + .collect(Collectors.toList()); + + + return MyPotResponseDTO.OngoingPotsDetail.builder() + .user(UserConverter.toDto(pot.getUser())) + .pot(potConverter.toDto(pot, pot.getRecruitmentDetails())) // 변환기 사용 + .potMembers(potMembers) + .build(); + } + @Transactional + @Override + public void removeMemberFromPot(Long potId) { + // 현재 로그인한 사용자 확인 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + // 현재 로그인한 사용자 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new IllegalArgumentException("현재 사용자를 찾을 수 없습니다.")); + + // 팟 존재 여부 확인 + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new IllegalArgumentException("해당 팟을 찾을 수 없습니다.")); + + // 팟 멤버 존재 여부 확인 + PotMember member = potMemberRepository.findByPotAndUser(pot, user) + .orElseThrow(() -> new IllegalArgumentException("해당 팟에 사용자가 존재하지 않습니다.")); + + // 팟 멤버 삭제 + potMemberRepository.delete(member); + } + + + @Transactional + @Override + public String removePotOrMember(Long potId) { + // 현재 로그인한 사용자 확인 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + // 현재 로그인한 사용자 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new IllegalArgumentException("현재 사용자를 찾을 수 없습니다.")); + + // 팟 존재 여부 확인 + Pot pot = potRepository.findById(potId) + .orElseThrow(() -> new IllegalArgumentException("해당 팟을 찾을 수 없습니다.")); + + // 팟 생성자인지 확인 + if (pot.getUser().equals(user)) { + // 팟 생성자일 경우 팟과 관련된 모든 데이터 삭제 + potRepository.delete(pot); + return "팟이 성공적으로 삭제되었습니다."; + } else { + // 팟 멤버인지 확인 + PotMember member = potMemberRepository.findByPotAndUser(pot, user) + .orElseThrow(() -> new IllegalArgumentException("해당 팟에 사용자가 존재하지 않습니다.")); + + // 팟 멤버 삭제 + potMemberRepository.delete(member); + return "팟 멤버가 성공적으로 삭제되었습니다."; + } + } + +} diff --git a/src/main/java/stackpot/stackpot/service/PotSummarizationService.java b/src/main/java/stackpot/stackpot/service/PotSummarizationService.java new file mode 100644 index 00000000..8516a49d --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/PotSummarizationService.java @@ -0,0 +1,46 @@ +package stackpot.stackpot.service; + +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import stackpot.stackpot.config.OpenAIConfig; + +import java.util.Collections; +import java.util.Map; + +@Service +public class PotSummarizationService { + private static final String OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"; + private final RestTemplate restTemplate; + private final OpenAIConfig openAIConfig; + + public PotSummarizationService(RestTemplate restTemplate, OpenAIConfig openAIConfig) { + this.restTemplate = restTemplate; + this.openAIConfig = openAIConfig; + } + + public String summarizeText(String text, int maxTokens) { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(openAIConfig.getApiKey()); + headers.setContentType(MediaType.APPLICATION_JSON); + + Map requestBody = Map.of( + "model", "gpt-4-turbo", + "messages", Collections.singletonList( + Map.of("role", "user", "content", text) + ), + "max_tokens", maxTokens + ); + + HttpEntity> entity = new HttpEntity<>(requestBody, headers); + ResponseEntity response = restTemplate.exchange(OPENAI_API_URL, HttpMethod.POST, entity, Map.class); + + Map responseBody = response.getBody(); + if (responseBody != null && responseBody.containsKey("choices")) { + Map choice = (Map) ((java.util.List) responseBody.get("choices")).get(0); + Map message = (Map) choice.get("message"); + return message.get("content"); + } + throw new RuntimeException("Failed to summarize text"); + } +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/service/SearchService.java b/src/main/java/stackpot/stackpot/service/SearchService.java new file mode 100644 index 00000000..b7473e44 --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/SearchService.java @@ -0,0 +1,11 @@ +package stackpot.stackpot.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import stackpot.stackpot.web.dto.FeedSearchResponseDto; +import stackpot.stackpot.web.dto.PotSearchResponseDto; + +public interface SearchService { + Page searchPots(String keyword, Pageable pageable); + Page searchFeeds(String keyword, Pageable pageable); +} diff --git a/src/main/java/stackpot/stackpot/service/SearchServiceImpl.java b/src/main/java/stackpot/stackpot/service/SearchServiceImpl.java new file mode 100644 index 00000000..101eacd9 --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/SearchServiceImpl.java @@ -0,0 +1,43 @@ +package stackpot.stackpot.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import stackpot.stackpot.converter.FeedConverter; +import stackpot.stackpot.converter.PotConverter; +import stackpot.stackpot.domain.Feed; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.repository.FeedRepository.FeedRepository; +import stackpot.stackpot.repository.PotRepository.PotRepository; +import stackpot.stackpot.web.dto.FeedSearchResponseDto; +import stackpot.stackpot.web.dto.PotSearchResponseDto; + +@Service +@RequiredArgsConstructor +public class SearchServiceImpl implements SearchService { + + private final PotRepository potRepository; + private final FeedRepository feedRepository; + private final PotConverter potConverter; + private final FeedConverter feedConverter; + + @Override + @Transactional(readOnly = true) + public Page searchPots(String keyword, Pageable pageable) { + Page pots = potRepository.searchByKeyword(keyword, pageable); + return pots.map(potConverter::toSearchDto); + } + + @Override + @Transactional(readOnly = true) + public Page searchFeeds(String keyword, Pageable pageable) { + Page feeds = feedRepository.findByTitleContainingOrContentContainingOrderByCreatedAtDesc(keyword, keyword, pageable); + + // FeedConverter를 사용해 DTO 변환 및 좋아요 개수 포함 + return feeds.map(feedConverter::toSearchDto); + } + +} + diff --git a/src/main/java/stackpot/stackpot/service/TodoCleanupService.java b/src/main/java/stackpot/stackpot/service/TodoCleanupService.java new file mode 100644 index 00000000..ef9fc1d0 --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/TodoCleanupService.java @@ -0,0 +1,27 @@ +package stackpot.stackpot.service; + +import jakarta.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import lombok.RequiredArgsConstructor; +import stackpot.stackpot.repository.TodoRepository; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class TodoCleanupService { + + private final TodoRepository todoRepository; + + // 매일 오전 3시 (03:00:00)에 실행 + @Scheduled(cron = "0 0 3 * * ?") + @Transactional // 트랜잭션 추가 + public void deleteOldTodos() { + LocalDateTime yesterday = LocalDateTime.now().minusDays(1); + int deletedCount = todoRepository.deleteByCreatedAtBefore(yesterday); + System.out.println("[" + LocalDateTime.now() + "] " + deletedCount + "개의 오래된 TODO 항목이 삭제되었습니다."); + } +} diff --git a/src/main/java/stackpot/stackpot/service/UserCommandService.java b/src/main/java/stackpot/stackpot/service/UserCommandService.java new file mode 100644 index 00000000..a885a53c --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/UserCommandService.java @@ -0,0 +1,20 @@ +package stackpot.stackpot.service; + +import stackpot.stackpot.domain.User; +import stackpot.stackpot.web.dto.UserMypageResponseDto; +import stackpot.stackpot.web.dto.UserRequestDto; +import stackpot.stackpot.web.dto.UserResponseDto; +import stackpot.stackpot.web.dto.UserUpdateRequestDto; + +public interface UserCommandService { + User joinUser(UserRequestDto.JoinDto request); + User saveNewUser(String email); + + UserResponseDto getMypages(); + + UserMypageResponseDto getUserMypage(Long userId, String dataType); + + UserResponseDto updateUserProfile(UserUpdateRequestDto requestDto); + + String createNickname(); +} diff --git a/src/main/java/stackpot/stackpot/service/UserCommandServiceImpl.java b/src/main/java/stackpot/stackpot/service/UserCommandServiceImpl.java new file mode 100644 index 00000000..90e2e55a --- /dev/null +++ b/src/main/java/stackpot/stackpot/service/UserCommandServiceImpl.java @@ -0,0 +1,176 @@ +package stackpot.stackpot.service; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import stackpot.stackpot.apiPayload.code.status.ErrorStatus; +import stackpot.stackpot.apiPayload.exception.handler.MemberHandler; +import stackpot.stackpot.converter.UserMypageConverter; +import stackpot.stackpot.domain.Feed; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.repository.FeedRepository.FeedRepository; +import stackpot.stackpot.repository.PotRepository.PotRepository; +import stackpot.stackpot.repository.UserRepository.UserRepository; +import stackpot.stackpot.web.dto.*; + +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class UserCommandServiceImpl implements UserCommandService{ + + private final UserRepository userRepository; + private final PotRepository potRepository; + private final FeedRepository feedRepository; + private final UserMypageConverter userMypageConverter; + private final PotSummarizationService potSummarizationService; + + @Override + @Transactional + public User joinUser(UserRequestDto.JoinDto request) { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + updateUserData(user, request); + + return userRepository.save(user); + } + + @Override + public User saveNewUser(String email) { + + return userRepository.findByEmail(email) + .orElseGet(() -> { + User newUser = User.builder() + .email(email) + .userTemperature(33) + .build(); + + return userRepository.save(newUser); + }); + } + + private void updateUserData(User user, UserRequestDto.JoinDto request) { + // 카카오 id + user.setKakaoId(request.getKakaoId()); + // 닉네임 + user.setNickname(request.getNickname()); + // 역할군 + user.setRole(request.getRole()); + // 관심사 + user.setInterest(request.getInterest()); + //한줄 소개 + user.setUserIntroduction(user.getRole()+"에 관심있는 "+user.getNickname()+"입니다."); + } + + @Override + public UserResponseDto getMypages() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + // User 정보를 UserResponseDto로 변환 + return UserResponseDto.builder() + .email(user.getEmail()) + .nickname(user.getNickname() + getVegetableNameByRole(user.getRole().name())) // 닉네임 + 역할 + .role(user.getRole()) + .interest(user.getInterest()) + .userTemperature(user.getUserTemperature()) + .kakaoId(user.getKakaoId()) + .userIntroduction(user.getUserIntroduction()) // 한 줄 소개 추가 + .build(); + } + + @Transactional + public UserMypageResponseDto getUserMypage(Long userId, String dataType) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + List completedPots = List.of(); + List feeds = List.of(); + + if (dataType == null || dataType.isBlank()) { + // 모든 데이터 반환 (pot + feed) + completedPots = potRepository.findByUserIdAndPotStatus(userId, "COMPLETED"); + feeds = feedRepository.findByUser_Id(userId); + } else if ("pot".equalsIgnoreCase(dataType)) { + // 팟 정보만 반환 + completedPots = potRepository.findByUserIdAndPotStatus(userId, "COMPLETED"); + } else if ("feed".equalsIgnoreCase(dataType)) { + // 피드 정보만 반환 + feeds = feedRepository.findByUser_Id(userId); + } else { + throw new IllegalArgumentException("Invalid data type. Use 'pot', 'feed', or leave empty for all data."); + } + + return userMypageConverter.toDto(user, completedPots, feeds); + } + + + + @Transactional + public UserResponseDto updateUserProfile(UserUpdateRequestDto requestDto) { + // 현재 로그인한 사용자 정보 가져오기 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + // 업데이트할 필드 적용 + if (requestDto.getRole() != null) { + user.setRole(requestDto.getRole()); + } + if (requestDto.getInterest() != null && !requestDto.getInterest().isEmpty()) { + user.setInterest(requestDto.getInterest()); + } + if (requestDto.getUserIntroduction() != null && !requestDto.getUserIntroduction().isEmpty()) { + user.setUserIntroduction(requestDto.getUserIntroduction()); + } + + // 저장 후 DTO로 변환하여 반환 + userRepository.save(user); + + return UserResponseDto.builder() + .email(user.getEmail()) + .nickname(user.getNickname() + getVegetableNameByRole(user.getRole().name())) // 닉네임 + 역할 + .role(user.getRole()) + .interest(user.getInterest()) + .userTemperature(user.getUserTemperature()) + .kakaoId(user.getKakaoId()) + .userIntroduction(user.getUserIntroduction()) + .build(); + } + + @Override + public String createNickname() { + String prompt = "“재미있고 긍정적인 형용사와 명사를 결합한 문구를 만들어 주세요. 형식은 ‘형용사 명사’입니다" + + "예를 들어, ‘잘 자라는 양파’, ‘힘이 넘치는 버섯’ 같은 느낌으로 작성해 주세요.”"; + + String nickname = potSummarizationService.summarizeText(prompt, 15); + + return nickname; + } + + // 역할에 따른 채소명을 반환하는 메서드 + private String getVegetableNameByRole(String role) { + Map roleToVegetableMap = Map.of( + "BACKEND", " 양파", + "FRONTEND", " 버섯", + "DESIGN", " 브로콜리", + "PLANNING", " 당근" + ); + return roleToVegetableMap.getOrDefault(role, "알 수 없음"); + } +} diff --git a/src/main/java/stackpot/stackpot/web/controller/FeedController.java b/src/main/java/stackpot/stackpot/web/controller/FeedController.java new file mode 100644 index 00000000..17b2a962 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/controller/FeedController.java @@ -0,0 +1,104 @@ +package stackpot.stackpot.web.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.annotation.*; +import stackpot.stackpot.converter.FeedConverter; +import stackpot.stackpot.domain.Feed; +import stackpot.stackpot.domain.enums.Category; +import stackpot.stackpot.service.FeedService; +import stackpot.stackpot.web.dto.*; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/feeds") +@RequiredArgsConstructor +@Tag(name = "Feed Management", description = "피드 관리 API") +public class FeedController { + + private final FeedService feedService; + private final FeedConverter feedConverter; + + + @Operation(summary = "(수정 필요) feed 작성 api") + @PostMapping("") + public ResponseEntity createFeeds(@Valid @RequestBody FeedRequestDto.createDto requset) { + // 정상 처리 + Feed feed = feedService.createFeed(requset); + Long feedId = feed.getFeedId(); + Long saveCount = feedService.getSaveCount(feedId); + Long likeCount = feedService.getLikeCount(feedId); + + FeedResponseDto.FeedDto response = feedConverter.feedDto(feed, likeCount+saveCount,likeCount); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @Operation(summary = "feed 미리보기 api ( 수정 필요 )") + @GetMapping("") + public ResponseEntity getPreViewFeeds( + @RequestParam(value = "category", required = false, defaultValue = "ALL") Category category, + @RequestParam(value = "sort", required = false, defaultValue = "new") String sort, + @RequestParam(value = "cursor", required = false) String cursor, + @RequestParam(value = "limit", defaultValue = "10") int limit) { + + FeedResponseDto.FeedPreviewList response = feedService.getPreViewFeeds(category, sort, cursor, limit); + return ResponseEntity.ok(response); + } + @Operation(summary = "feed 상세보기 api") + @PostMapping("/{feedId}") + public ResponseEntity getDetailFeed(@PathVariable Long feedId) { + + Feed feed = feedService.getFeed(feedId); + Long saveCount = feedService.getSaveCount(feedId); + Long likeCount = feedService.getLikeCount(feedId); + + FeedResponseDto.FeedDto response = feedConverter.feedDto(feed, likeCount+saveCount,likeCount); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @Operation(summary = "feed 수정 api (수정 필요") + @PatchMapping("/{feedId}") + public ResponseEntity modifyFeed(@PathVariable Long feedId, @Valid @RequestBody FeedRequestDto.createDto requset) { + // 정상 처리 + Feed feed = feedService.modifyFeed(feedId, requset); + Long saveCount = feedService.getSaveCount(feedId); + Long likeCount = feedService.getLikeCount(feedId); + + FeedResponseDto.FeedDto response =feedConverter.feedDto(feed, likeCount+saveCount,likeCount); + + return ResponseEntity.ok(response); + } + @Operation(summary = "feed 좋아요 추가 api") + @PostMapping("/{feedId}/like") + public ResponseEntity toggleLike(@PathVariable Long feedId) { + + // 좋아요 토글 + boolean isLiked = feedService.toggleLike(feedId); + return ResponseEntity.ok(Map.of( + "liked", isLiked, + "message", isLiked ? "좋아요를 눌렀습니다." : "좋아요를 취소했습니다." + )); + } + + @Operation(summary = "feed 저장하기 api") + @PostMapping("/{feedId}/save") + public ResponseEntity toggleSave(@PathVariable Long feedId) { + + // 좋아요 토글 + boolean isSaved = feedService.toggleSave(feedId); + return ResponseEntity.ok(Map.of( + "saved", isSaved, + "message", isSaved ? "해당 피드를 저장했습니다." : "해당 피드 저장을 취소했습니다." + )); + } + +} diff --git a/src/main/java/stackpot/stackpot/web/controller/MyPotController.java b/src/main/java/stackpot/stackpot/web/controller/MyPotController.java new file mode 100644 index 00000000..0ff468ff --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/controller/MyPotController.java @@ -0,0 +1,140 @@ +package stackpot.stackpot.web.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import stackpot.stackpot.apiPayload.ApiResponse; +import stackpot.stackpot.service.MyPotService; +import stackpot.stackpot.service.PotService; +import stackpot.stackpot.web.dto.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequiredArgsConstructor +@Tag(name = "My Pot Management", description = "나의 팟 관리 API") +public class MyPotController { + + private final MyPotService myPotService; + private final PotService potService; + + // 사용자가 만든 진행 중인 팟 조회 + @Operation(summary = "사용자의 팟 목록 조회 API", description = "사용자가 생성했거나, 참여하고 있으며 진행 중(ONGOING)인 팟들 리스트를 조회합니다. \n") + @GetMapping("/my-pots") + public ResponseEntity>>> getMyOngoingPots() { + Map> response = myPotService.getMyOnGoingPots(); + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } +// @DeleteMapping("/{pot_id}/members") +// @Operation(summary = "팟에서 본인 삭제", description = "현재 로그인한 팟 멤버가 본인의 팟을 삭제하면 팟 멤버에서 자신이 제거됩니다.") +// public ResponseEntity> removePotMember( +// @PathVariable("pot_id") Long potId) { +// +// potService.removeMemberFromPot(potId); +// return ResponseEntity.ok(ApiResponse.onSuccess("팟 멤버가 성공적으로 삭제되었습니다.")); +// } + @DeleteMapping("/{pot_id}/members") + @Operation(summary = "팟 멤버 삭제 또는 팟 삭제", description = "생성자는 팟을 삭제하며, 생성자가 아니면 팟 멤버에서 본인을 삭제합니다.") + public ResponseEntity> removePotOrMember( + @PathVariable("pot_id") Long potId) { + + String responseMessage = potService.removePotOrMember(potId); + return ResponseEntity.ok(ApiResponse.onSuccess(responseMessage)); + } + + // 팟에서의 투두 생성 + @Operation( + summary = "Todo 생성 API", + description = """ + - Status: NOT_STARTED / COMPLETED + * 생성의 경우 NOT_STARTED로 전달해 주시면 됩니다. + """ + ) + @PostMapping("/my-pots/{pot_id}/todos") + public ResponseEntity>> postMyTodo( + @PathVariable("pot_id") Long potId, + @RequestBody MyPotTodoRequestDTO request) { + + List response = myPotService.postTodo(potId, request); + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + + // 팟에서의 투두 조회 + @Operation(summary = "Todo 조회 API") + @GetMapping("/my-pots/{pot_id}/todos") + public ResponseEntity>> getMyTodo(@PathVariable("pot_id") Long potId){ + List response = myPotService.getTodo(potId); // 수정된 부분 + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + + @Operation(summary = "Todo 내용 일괄 수정 API", description = "사용자의 모든 투두의 내용을 한 번에 수정할 수 있습니다. 리스트 사이에 ,로 구분해서 전달해 주셔야 합니다!") + @PatchMapping("/my-pots/{pot_id}/todos") + public ResponseEntity>> updateMyTodos( + @PathVariable("pot_id") Long potId, + @RequestBody List requestList) { + + List response = myPotService.updateTodos(potId, requestList); + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + + @Operation(summary = "mypotTask 생성 API") + @PostMapping("/my-pots/{pot_id}/tasks") + public ResponseEntity> createPotTask(@PathVariable("pot_id") Long potId, + @RequestBody @Valid MyPotTaskRequestDto.create request) { + MyPotTaskResponseDto response = myPotService.creatTask(potId, request); + + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + @Operation(summary = "mypotTask 상세보기 API") + @GetMapping("/my-pots/{pot_id}/tasks/{task_id}") + public ResponseEntity> getPotDetailTask(@PathVariable("pot_id") Long potId, @PathVariable("task_id") Long taskId) { + + MyPotTaskResponseDto response = myPotService.viewDetailTask(taskId); + + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + + @Operation(summary = "[미완성] mypotTask 불러오기 API") + @GetMapping("/my-pots/{pot_id}/tasks") + public ResponseEntity getPotTask(@PathVariable("pot_id") Long potId) { + + return null; + } + + @Operation(summary = "mypotTask 수정 API") + @PatchMapping("/my-pots/{pot_id}/tasks/{task_id}") + public ResponseEntity> modifyPotTask(@PathVariable("task_id") Long taskId, @RequestBody @Valid MyPotTaskRequestDto.create request) { + MyPotTaskResponseDto response = myPotService.modfiyTask(taskId, request); + + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + + @Operation(summary = "mypotTask 삭제 API") + @DeleteMapping("/my-pots/{pot_id}/tasks/{task_id}") + public ResponseEntity deletetPotTask(@PathVariable("pot_id") Long potId, @PathVariable("task_id") Long taskId) { + try { + myPotService.deleteTaskboard(potId, taskId); + return ResponseEntity.ok(ApiResponse.onSuccess("할일이 삭제되었습니다.")); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("An error occurred while deleting the taskboard and associated tasks."); + } + } + @Operation(summary = "Todo 완료 API", description = "todo의 status를 COMPLETED로 변경합니다.") + @PatchMapping("/my-pots/{pot_id}/todos/{todo_id}") + public ResponseEntity>> completeTodo( + @PathVariable("pot_id") Long potId, + @PathVariable("todo_id") Long todoId) { + + List response = myPotService.completeTodo(potId, todoId); + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + +} diff --git a/src/main/java/stackpot/stackpot/web/controller/PotApplicationController.java b/src/main/java/stackpot/stackpot/web/controller/PotApplicationController.java new file mode 100644 index 00000000..25a3503c --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/controller/PotApplicationController.java @@ -0,0 +1,44 @@ +package stackpot.stackpot.web.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import stackpot.stackpot.service.PotApplicationService.PotApplicationService; +import stackpot.stackpot.web.dto.PotApplicationRequestDto; +import stackpot.stackpot.web.dto.PotApplicationResponseDto; + +import java.util.List; + +@Tag(name = "Pot Application Management", description = "팟 지원 관리 API") +@RestController +@RequestMapping("/pots/{pot_id}/applications") +@RequiredArgsConstructor +public class PotApplicationController { + + private final PotApplicationService potApplicationService; + @Operation(summary = "팟 지원하기") + @PostMapping + public ResponseEntity applyToPot( + @PathVariable("pot_id") Long potId, + @RequestBody @Valid PotApplicationRequestDto requestDto) { + + // 팟 지원 로직 호출 + PotApplicationResponseDto responseDto = potApplicationService.applyToPot(requestDto, potId); + + return ResponseEntity.ok(responseDto); // 성공 시 응답 반환 + } + + @Operation(summary = "팟 지원자 조회하기") + @GetMapping("") + public ResponseEntity> getApplicants( + @PathVariable("pot_id") Long potId) { + // 서비스 호출 + List applicants = potApplicationService.getApplicantsByPotId(potId); + + return ResponseEntity.ok(applicants); + } + +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/web/controller/PotController.java b/src/main/java/stackpot/stackpot/web/controller/PotController.java new file mode 100644 index 00000000..f300e099 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/controller/PotController.java @@ -0,0 +1,188 @@ +package stackpot.stackpot.web.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import stackpot.stackpot.apiPayload.code.status.ErrorStatus; +import stackpot.stackpot.apiPayload.exception.handler.EnumHandler; +import stackpot.stackpot.apiPayload.exception.handler.RecruitmentHandler; +import stackpot.stackpot.apiPayload.ApiResponse; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.enums.Role; +import stackpot.stackpot.repository.PotRepository.PotRepository; +import stackpot.stackpot.service.PotService; +import stackpot.stackpot.service.PotServiceImpl; +import stackpot.stackpot.web.dto.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Tag(name = "Pot Management", description = "팟 관리 API") +@RestController +@RequestMapping("/pots") +@RequiredArgsConstructor +public class PotController { + + + private final PotService potService1; + + private final PotServiceImpl potService; + private final PotRepository potRepository; + + @Operation( + summary = "팟 생성하기", + description = """ + - potStatus: RECRUITING / ONGOING / COMPLETED + - potModeOfOperation: ONLINE / OFFLINE / HYBRID + - Role: FRONTEND / BACKEND / DESIGN / PLANNING + """ + ) @PostMapping + public ResponseEntity createPot( + + @RequestBody @Valid PotRequestDto requestDto) { + // 팟 생성 로직 호출 + PotResponseDto responseDto = potService.createPotWithRecruitments(requestDto); + + return ResponseEntity.ok(responseDto); + } + + @Operation(summary = "팟 수정하기") + @PatchMapping("/{pot_id}") + public ResponseEntity updatePot( + @PathVariable("pot_id") Long potId, + @RequestBody @Valid PotRequestDto requestDto) { + // 팟 수정 로직 호출 + PotResponseDto responseDto = potService.updatePotWithRecruitments(potId, requestDto); + + return ResponseEntity.ok(responseDto); // 수정된 팟 정보 반환 + } + + @Operation(summary = "팟 삭제하기") + @DeleteMapping("/{pot_id}") + public ResponseEntity deletePot(@PathVariable("pot_id") Long potId) { + // 팟 삭제 로직 호출 + potService.deletePot(potId); + + return ResponseEntity.noContent().build(); + } + + + + @GetMapping("/completed") + @Operation(summary = "나의 끓인 팟 정보 가져오기", description = "potStatus가 COMPLETED인 팟의 목록을 커서 기반 페이지네이션으로 가져옵니다.", + parameters = { + @Parameter(name = "cursor", description = "현재 페이지의 마지막 potId 값", example = "10"), + @Parameter(name = "size", description = "한 페이지에 가져올 데이터 개수", example = "3") + }) + public ResponseEntity>> getMyCompletedPots( + @RequestParam(value = "cursor", required = false) Long cursor, + @RequestParam(value = "size", defaultValue = "3") int size) { + CursorPageResponse response = potService.getMyCompletedPots(cursor, size); + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + + //------------------- + + @Operation( + summary = "팟 전체 보기 API", + description = """ + - Role: FRONTEND / BACKEND / DESIGN / PLANNING / (NULL) + 만약 null인 경우 모든 role에 대해서 조회합니다. + """ + ) + @GetMapping + public ResponseEntity>> getPots( + @RequestParam(required = false) String recruitmentRole, + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + + if (page < 1) { + throw new EnumHandler(ErrorStatus.INVALID_PAGE); + } + + Role roleEnum = null; + if (recruitmentRole != null && !recruitmentRole.isEmpty()) { + try { + roleEnum = Role.valueOf(recruitmentRole.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + throw new RecruitmentHandler(ErrorStatus.INVALID_ROLE); + } + } + + int adjustedPage = page - 1; + + List pots = potService1.getAllPots(roleEnum, adjustedPage, size); + + Page potPage = (roleEnum == null) + ? potRepository.findAll(PageRequest.of(adjustedPage, size)) + : potRepository.findByRecruitmentDetails_RecruitmentRole(roleEnum, PageRequest.of(adjustedPage, size)); + + Map response = new HashMap<>(); + response.put("pots", pots); + response.put("totalPages", potPage.getTotalPages()); + response.put("currentPage", potPage.getNumber() + 1); + response.put("totalElements", potPage.getTotalElements()); + + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + + // 특정 팟의 상세정보 조회 + @Operation(summary = "특정 팟의 상세정보 조회 API", description = "potId를 통해 특정 팟에 대한 상세정보를 조회할 수 있습니다. ") + @GetMapping("/{pot_id}") + public ResponseEntity> getPotDetails(@PathVariable("pot_id") Long potId) { + ApplicantResponseDTO potDetails = potService1.getPotDetails(potId); + return ResponseEntity.ok(ApiResponse.onSuccess(potDetails)); + } + + // 특정 팟 지원자의 좋아요 상태 변경 + @Operation(summary = "특정 팟 지원자의 '마음에 들어요' 상태 변경 API", description = "지원자의 아이디와 liked 값을 true, false (Boolean)로 request 해 주시면 지원자의 liked 상태값이 해당 값에 맞춰 변경됩니다.") + @PatchMapping("/{pot_id}/applications/like") + public ResponseEntity> patchLikes( + @PathVariable("pot_id") Long potId, + @RequestBody LikeRequestDTO likeRequest) { + potService1.patchLikes(potId, likeRequest.getApplicationId(), likeRequest.getLiked()); + return ResponseEntity.ok(ApiResponse.onSuccess(null)); + } + + // 특정 팟의 좋아요한 지원자 목록 조회 + @Operation(summary = "특정 팟의 '마음에 들어요' 지원자들 목록 조회 API", description = "지원자의 id, pot 지원 역할, 지원 역할에 따른 팟에서의 nickname, like 상태 값을 반환합니다. ") + @GetMapping("/{pot_id}/applications/like") + public ResponseEntity>> getLikedApplicants( + @PathVariable("pot_id") Long potId) { + List likedApplicants = potService1.getLikedApplicants(potId); + return ResponseEntity.ok(ApiResponse.onSuccess(likedApplicants)); + } + + // 사용자가 지원한 팟 조회 + @Operation(summary = "사용자가 지원한 팟 조회 API") + @GetMapping("/apply") + public ResponseEntity>> getAppliedPots() { + List appliedPots = potService1.getAppliedPots(); + return ResponseEntity.ok(ApiResponse.onSuccess(appliedPots)); + } + + // 사용자가 만든 팟 조회 + @Operation(summary = "사용자가 만든 팟 조회 API", description = "모집 중인 나의 팟, 진행 중인 나의 팟, 끓인 나의 팟을 구분하여 리스트 형식으로 전달합니다. 진행 중인 팟의 경우 멤버들의 사진이 보여야 하기에 potMembers 정보를 함께 전달합니다.") + @GetMapping("/my-pots") + public ResponseEntity>> getMyPots() { + List myPots = potService1.getMyPots(); + return ResponseEntity.ok(ApiResponse.onSuccess(myPots)); + } + + + // Pot 내용 AI 요약 + @Operation(summary = "Pot 내용 AI 요약 API", description = "팟의 구인글 내용을 활용해 작성됩니다.") + @GetMapping("/{pot_id}/summary") + public ResponseEntity> getPotSummary(@PathVariable("pot_id") Long potId) { + PotSummaryResponseDTO summary = potService1.getPotSummary(potId); + return ResponseEntity.ok(ApiResponse.onSuccess(summary)); + } + +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/web/controller/PotMemberController.java b/src/main/java/stackpot/stackpot/web/controller/PotMemberController.java new file mode 100644 index 00000000..cc4203bc --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/controller/PotMemberController.java @@ -0,0 +1,57 @@ +package stackpot.stackpot.web.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import stackpot.stackpot.apiPayload.ApiResponse; +import stackpot.stackpot.service.PotMemberService.PotMemberService; +import stackpot.stackpot.web.dto.PotMemberRequestDto; +import stackpot.stackpot.web.dto.PotMemberAppealResponseDto; +import stackpot.stackpot.web.dto.UpdateAppealRequestDto; + +import java.util.List; + +@RestController +@RequestMapping("/pots/{pot_id}/members") +@RequiredArgsConstructor +@Tag(name = "Pot Member Management", description = "팟 멤버 관리 API") +public class PotMemberController { + + private final PotMemberService potMemberService; + + @Operation(summary = "팟 멤버 정보 가져오기( KAKAOID, 닉네임)") + @GetMapping + public ResponseEntity>> getPotMembers( + @PathVariable("pot_id") Long potId) { + potMemberService.validateIsOwner(potId); // 팟 생성자 검증 추가 + List response = potMemberService.getPotMembers(potId); + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + + @Operation( + summary = "팟 시작하기", + description = "지원자 ID 리스트를 받아 팟 멤버를 추가합니다." + + ) + @PostMapping + public ResponseEntity>> addPotMembers( + @PathVariable("pot_id") Long potId, + @RequestBody @Valid PotMemberRequestDto requestDto) { + List response = potMemberService.addMembersToPot(potId, requestDto); + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + @Operation(summary = "팟 어필하기") + @PatchMapping("/{member_id}/appeal") + public ResponseEntity> updateAppealContent( + @PathVariable("pot_id") Long potId, + @PathVariable("member_id") Long memberId, + @RequestBody @Valid UpdateAppealRequestDto requestDto) { + potMemberService.updateAppealContent(potId, memberId, requestDto.getAppealContent()); + return ResponseEntity.ok(ApiResponse.onSuccess("어필 내용이 성공적으로 업데이트되었습니다.")); + } +} diff --git a/src/main/java/stackpot/stackpot/web/controller/SearchController.java b/src/main/java/stackpot/stackpot/web/controller/SearchController.java new file mode 100644 index 00000000..0dc261d7 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/controller/SearchController.java @@ -0,0 +1,108 @@ +package stackpot.stackpot.web.controller; + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import stackpot.stackpot.apiPayload.ApiResponse; +import stackpot.stackpot.service.SearchService; +import stackpot.stackpot.web.dto.FeedSearchResponseDto; +import stackpot.stackpot.web.dto.PotSearchResponseDto; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/search") +@RequiredArgsConstructor +@Tag(name = "Search Management", description = "검색 API") +public class SearchController { + + private final SearchService searchService; + + @GetMapping("/pots") + @Operation(summary = "팟 검색", description = "키워드로 팟 이름 및 내용을 검색합니다.", + parameters = { + @Parameter(name = "keyword", description = "검색 키워드", example = "JAVA"), + + }) + public ResponseEntity>> searchPots( + @RequestParam(required = false, defaultValue = "") String keyword, + @PageableDefault(size = 3, sort = "createdAt") Pageable pageable){ + + + + // 키워드가 없는 경우 예외 처리 또는 전체 조회 처리 +// if (keyword.trim().isEmpty()) { +// return ResponseEntity.badRequest() +// .body(ApiResponse.onError("400", "검색 키워드를 입력해주세요.")); +// } + + // 서비스 호출 및 검색 수행 + Page response = searchService.searchPots(keyword, pageable); + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + + + @Operation(summary = "피드 검색", description = "키워드로 피드 제목 및 내용을 검색합니다.", + parameters = { + @Parameter(name = "keyword", description = "검색 키워드", example = "Spring"), + @Parameter(name = "page", description = "요청 페이지 번호 (0부터 시작)", example = "0"), + @Parameter(name = "size", description = "한 페이지에 가져올 데이터 개수", example = "10") + }) + @GetMapping("/feeds") + public ResponseEntity>> searchFeeds( + @RequestParam("keyword") String keyword, + @RequestParam(defaultValue = "0") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + + Page feedPage = searchService.searchFeeds(keyword, PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"))); + + Map response = new HashMap<>(); + response.put("feeds", feedPage.getContent()); + response.put("totalPages", feedPage.getTotalPages()); + response.put("currentPage", feedPage.getNumber()); + response.put("totalElements", feedPage.getTotalElements()); + + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + + + @GetMapping + @Operation(summary = "팟 or 피드 검색 API", description = "키워드를 기반으로 팟 또는 피드를 검색합니다.", + parameters = { + @Parameter(name = "type", description = "검색 타입 (pot: 팟 검색, feed: 피드 검색)", example = "pot"), + @Parameter(name = "keyword", description = "검색 키워드", example = "JAVA"), + @Parameter(name = "page", description = "페이지 번호", example = "0"), + @Parameter(name = "size", description = "페이지 크기", example = "10") + }) + public ResponseEntity>> search( + @RequestParam String type, + @RequestParam String keyword, + @PageableDefault(size = 10) Pageable pageable) { + + Page response; + if ("pot".equalsIgnoreCase(type)) { + response = searchService.searchPots(keyword, pageable); + } else if ("feed".equalsIgnoreCase(type)) { + response = searchService.searchFeeds(keyword, pageable); + } else { + throw new IllegalArgumentException("Invalid search type. Use 'pot' or 'feed'."); + } + + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + + +} diff --git a/src/main/java/stackpot/stackpot/web/controller/UserController.java b/src/main/java/stackpot/stackpot/web/controller/UserController.java new file mode 100644 index 00000000..a4353278 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/controller/UserController.java @@ -0,0 +1,146 @@ +package stackpot.stackpot.web.controller; + +import io.swagger.v3.oas.annotations.Operation; +//import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.annotation.*; +import stackpot.stackpot.apiPayload.ApiResponse; +import stackpot.stackpot.config.security.JwtTokenProvider; +import stackpot.stackpot.converter.UserConverter; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.repository.UserRepository.UserRepository; +import stackpot.stackpot.service.KakaoService; +import stackpot.stackpot.service.UserCommandService; +import stackpot.stackpot.web.dto.*; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; +@Tag(name = "User Management", description = "유저 관리 API") +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/users") +public class UserController { + + private final UserCommandService userCommandService; + private final UserRepository userRepository; + private final KakaoService kakaoService; + private final JwtTokenProvider jwtTokenProvider; + @Operation(summary = "토큰 test api") + @GetMapping("/login/token") + public ResponseEntity testEndpoint(Authentication authentication) { + if (authentication == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("User is not authenticated"); + } + return ResponseEntity.ok("Authenticated user: " + authentication.getName()); + } + +// @GetMapping("/oauth/kakao") +// public ResponseEntity callback(@RequestParam("code") String code) { +// +// log.info("Authorization code: {}", code); // 인증 코드 확인 +// String accessToken = kakaoService.getAccessTokenFromKakao(code); +// KakaoUserInfoResponseDto userInfo = kakaoService.getUserInfo(accessToken); +// +// String email = userInfo.getKakaoAccount().getEmail();// 이메일 가져오기 +// log.info("userInfo.getEmail -> ", email); +// +// User user = userCommandService.saveNewUser(email); +// +// TokenServiceResponse token = jwtTokenProvider.createToken(user); +// log.info("STACKPOT ACESSTOKEN : " + token.getAccessToken()); +// +// +// return ResponseEntity.ok(token); +// } + + @GetMapping("/oauth/kakao") + public void callback(@RequestParam("code") String code, HttpServletResponse response) throws IOException { + + log.info("Authorization code: {}", code); + String accessToken = kakaoService.getAccessTokenFromKakao(code); + KakaoUserInfoResponseDto userInfo = kakaoService.getUserInfo(accessToken); + + String email = userInfo.getKakaoAccount().getEmail(); + log.info("userInfo.getEmail -> {}", email); + + User user = userCommandService.saveNewUser(email); + + TokenServiceResponse token = jwtTokenProvider.createToken(user); + log.info("AccessToken: {}", token.getAccessToken()); + + if (user.getId() == null) { + // 미가입 유저: 회원가입 페이지로 리다이렉트 (토큰을 헤더로 추가) + response.setHeader("Authorization", "Bearer " + token.getAccessToken()); + response.sendRedirect("/sign-up"); + } else { + // 가입된 유저: 홈 페이지로 리다이렉트 (토큰을 헤더로 추가) + response.setHeader("Authorization", "Bearer " + token.getAccessToken()); + response.sendRedirect("/callback"); + } + } + + @Operation(summary = "회원가입 api") + @PatchMapping("/profile") + public ResponseEntity signup(@Valid @RequestBody UserRequestDto.JoinDto request, + BindingResult bindingResult) { + // 유효성 검사 실패 처리 + if (bindingResult.hasErrors()) { + // 에러 메시지 수집 + List errors = bindingResult.getAllErrors() + .stream() + .map(ObjectError::getDefaultMessage) + .collect(Collectors.toList()); + return ResponseEntity.badRequest().body(errors); + } + // 정상 처리 + User user = userCommandService.joinUser(request); + return ResponseEntity.status(HttpStatus.CREATED).body(UserConverter.toDto(user)); + } + + @Operation(summary = "닉네임 생성 [질문 수정 필요]") + @GetMapping("/nickname") + public ResponseEntity> nickname(){ + String nickName = userCommandService.createNickname(); + + return ResponseEntity.ok(ApiResponse.onSuccess(nickName)); + } + + @Operation(summary = "마이페이지 사용자 정보 조회 API") + @GetMapping("/mypages") + public ResponseEntity> usersMypages(){ + UserResponseDto userDetails = userCommandService.getMypages(); + return ResponseEntity.ok(ApiResponse.onSuccess(userDetails)); + } + + @Operation(summary = "다른 사람 마이페이지(프로필) 조회 API", description = "dataType = pot / feed / (null : pot + feed)") + @GetMapping("/{userId}/mypages") + public ResponseEntity> getUserMypage( + @PathVariable Long userId, + @RequestParam(required = false) String dataType) { + UserMypageResponseDto response = userCommandService.getUserMypage(userId, dataType); + return ResponseEntity.ok(ApiResponse.onSuccess(response)); + } + + @PatchMapping("/profile/update") + @Operation(summary = "사용자 프로필 수정 API", description = "사용자의 역할, 관심사, 한 줄 소개를 수정합니다.") + public ResponseEntity> updateUserProfile( + @RequestBody @Valid UserUpdateRequestDto requestDto) { + + UserResponseDto updatedUser = userCommandService.updateUserProfile(requestDto); + return ResponseEntity.ok(ApiResponse.onSuccess(updatedUser)); + } + + +} diff --git a/src/main/java/stackpot/stackpot/web/dto/ApplicantResponseDTO.java b/src/main/java/stackpot/stackpot/web/dto/ApplicantResponseDTO.java new file mode 100644 index 00000000..f9568988 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/ApplicantResponseDTO.java @@ -0,0 +1,23 @@ +package stackpot.stackpot.web.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; +import java.util.List; + +@Builder +@Getter +public class ApplicantResponseDTO { + private UserResponseDto user; + private PotResponseDto pot; + private List applicant; + + @Getter + @Builder + public static class ApplicantDto { + private Long applicationId; + private String potRole; + private Boolean liked; + } +} diff --git a/src/main/java/stackpot/stackpot/web/dto/CompletedPotResponseDto.java b/src/main/java/stackpot/stackpot/web/dto/CompletedPotResponseDto.java new file mode 100644 index 00000000..e8e58f45 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/CompletedPotResponseDto.java @@ -0,0 +1,34 @@ +package stackpot.stackpot.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Builder; +import lombok.Setter; +import stackpot.stackpot.domain.enums.PotModeOfOperation; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CompletedPotResponseDto { + private Long potId; // 팟 ID + private String potName; // 팟 이름 + private LocalDate potStartDate; // 시작 날짜 + private LocalDate potEndDate; // 종료 날짜 + private String potDuration; // 팟 기간 설명 + private String potLan; // 사용 언어 + private String potContent; // 팟 설명 + private String potStatus; // 팟 상태 + private PotModeOfOperation potModeOfOperation; // 운영 방식 + private String potSummary; // 요약 설명 + private LocalDate recruitmentDeadline; // 모집 마감일 + private List recruitmentDetails; // 수정된 부분 // 모집 세부 정보 + private Map roleCounts; // 역할별 인원 +} + diff --git a/src/main/java/stackpot/stackpot/web/dto/CursorPageResponse.java b/src/main/java/stackpot/stackpot/web/dto/CursorPageResponse.java new file mode 100644 index 00000000..b4aa3fad --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/CursorPageResponse.java @@ -0,0 +1,19 @@ +package stackpot.stackpot.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class CursorPageResponse { + + private List content; // 데이터 내용 + private Long nextCursor; // 다음 페이지를 가져올 커서 값 + private boolean hasMore; // 다음 데이터 존재 여부 +} diff --git a/src/main/java/stackpot/stackpot/web/dto/FeedRequestDto.java b/src/main/java/stackpot/stackpot/web/dto/FeedRequestDto.java new file mode 100644 index 00000000..58463c1f --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/FeedRequestDto.java @@ -0,0 +1,24 @@ +package stackpot.stackpot.web.dto; + +import jakarta.persistence.Column; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import stackpot.stackpot.domain.enums.Category; +import stackpot.stackpot.domain.enums.Role; +import stackpot.stackpot.domain.enums.Visibility; + +public class FeedRequestDto { + @Getter + @Setter + @NoArgsConstructor + public static class createDto { + private String title; + private String content; + private Category categor; + private Visibility visibility; + } +} diff --git a/src/main/java/stackpot/stackpot/web/dto/FeedResponseDto.java b/src/main/java/stackpot/stackpot/web/dto/FeedResponseDto.java new file mode 100644 index 00000000..22a1a591 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/FeedResponseDto.java @@ -0,0 +1,39 @@ +package stackpot.stackpot.web.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; +import stackpot.stackpot.domain.enums.Category; + +import java.time.LocalDateTime; +import java.util.List; + +public class FeedResponseDto { + + @Data + @Getter + @Setter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class FeedPreviewList { + private List feeds; + private String nextCursor; // 다음 커서 값 + } + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class FeedDto { + private Long id; + private String writer; + private Category category; + private String title; + private String content; + private Long popularity; + private Long likeCount; + private String createdAt; + } + + +} diff --git a/src/main/java/stackpot/stackpot/web/dto/FeedSearchResponseDto.java b/src/main/java/stackpot/stackpot/web/dto/FeedSearchResponseDto.java new file mode 100644 index 00000000..92787cb0 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/FeedSearchResponseDto.java @@ -0,0 +1,18 @@ +package stackpot.stackpot.web.dto; + +import lombok.*; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FeedSearchResponseDto { + private Long feedId; + private String title; + private String content; + private String creatorNickname; + private String creatorRole; + private String createdAt; + private Long likeCount; +} diff --git a/src/main/java/stackpot/stackpot/web/dto/KakaoTokenResponseDto.java b/src/main/java/stackpot/stackpot/web/dto/KakaoTokenResponseDto.java new file mode 100644 index 00000000..2727d113 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/KakaoTokenResponseDto.java @@ -0,0 +1,28 @@ +package stackpot.stackpot.web.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor //역직렬화를 위한 기본 생성자 +@JsonIgnoreProperties(ignoreUnknown = true) +public class KakaoTokenResponseDto { + + @JsonProperty("token_type") + public String tokenType; + @JsonProperty("access_token") + public String accessToken; + @JsonProperty("id_token") + public String idToken; + @JsonProperty("expires_in") + public Integer expiresIn; + @JsonProperty("refresh_token") + public String refreshToken; + @JsonProperty("refresh_token_expires_in") + public Integer refreshTokenExpiresIn; + @JsonProperty("scope") + public String scope; +} + diff --git a/src/main/java/stackpot/stackpot/web/dto/KakaoUserInfoResponseDto.java b/src/main/java/stackpot/stackpot/web/dto/KakaoUserInfoResponseDto.java new file mode 100644 index 00000000..a91fa3f6 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/KakaoUserInfoResponseDto.java @@ -0,0 +1,190 @@ +package stackpot.stackpot.web.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Date; +import java.util.HashMap; + +@Getter +@NoArgsConstructor //역직렬화를 위한 기본 생성자 +@JsonIgnoreProperties(ignoreUnknown = true) +public class KakaoUserInfoResponseDto { + + //회원 번호 + @JsonProperty("id") + public Long id; + + //자동 연결 설정을 비활성화한 경우만 존재. + //true : 연결 상태, false : 연결 대기 상태 + @JsonProperty("has_signed_up") + public Boolean hasSignedUp; + + //서비스에 연결 완료된 시각. UTC + @JsonProperty("connected_at") + public Date connectedAt; + + //카카오싱크 간편가입을 통해 로그인한 시각. UTC + @JsonProperty("synched_at") + public Date synchedAt; + + //사용자 프로퍼티 + @JsonProperty("properties") + public HashMap properties; + + //카카오 계정 정보 + @JsonProperty("kakao_account") + public KakaoAccount kakaoAccount; + + //uuid 등 추가 정보 + @JsonProperty("for_partner") + public Partner partner; + + @Getter + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public class KakaoAccount { + + //프로필 정보 제공 동의 여부 + @JsonProperty("profile_needs_agreement") + public Boolean isProfileAgree; + + //닉네임 제공 동의 여부 + @JsonProperty("profile_nickname_needs_agreement") + public Boolean isNickNameAgree; + + //프로필 사진 제공 동의 여부 + @JsonProperty("profile_image_needs_agreement") + public Boolean isProfileImageAgree; + + //사용자 프로필 정보 + @JsonProperty("profile") + public Profile profile; + + //이름 제공 동의 여부 + @JsonProperty("name_needs_agreement") + public Boolean isNameAgree; + + //카카오계정 이름 + @JsonProperty("name") + public String name; + + //이메일 제공 동의 여부 + @JsonProperty("email_needs_agreement") + public Boolean isEmailAgree; + + //이메일이 유효 여부 + // true : 유효한 이메일, false : 이메일이 다른 카카오 계정에 사용돼 만료 + @JsonProperty("is_email_valid") + public Boolean isEmailValid; + + //이메일이 인증 여부 + //true : 인증된 이메일, false : 인증되지 않은 이메일 + @JsonProperty("is_email_verified") + public Boolean isEmailVerified; + + //카카오계정 대표 이메일 + @JsonProperty("email") + public String email; + + //연령대 제공 동의 여부 + @JsonProperty("age_range_needs_agreement") + public Boolean isAgeAgree; + + //연령대 + //참고 https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info + @JsonProperty("age_range") + public String ageRange; + + //출생 연도 제공 동의 여부 + @JsonProperty("birthyear_needs_agreement") + public Boolean isBirthYearAgree; + + //출생 연도 (YYYY 형식) + @JsonProperty("birthyear") + public String birthYear; + + //생일 제공 동의 여부 + @JsonProperty("birthday_needs_agreement") + public Boolean isBirthDayAgree; + + //생일 (MMDD 형식) + @JsonProperty("birthday") + public String birthDay; + + //생일 타입 + // SOLAR(양력) 혹은 LUNAR(음력) + @JsonProperty("birthday_type") + public String birthDayType; + + //성별 제공 동의 여부 + @JsonProperty("gender_needs_agreement") + public Boolean isGenderAgree; + + //성별 + @JsonProperty("gender") + public String gender; + + //전화번호 제공 동의 여부 + @JsonProperty("phone_number_needs_agreement") + public Boolean isPhoneNumberAgree; + + //전화번호 + //국내 번호인 경우 +82 00-0000-0000 형식 + @JsonProperty("phone_number") + public String phoneNumber; + + //CI 동의 여부 + @JsonProperty("ci_needs_agreement") + public Boolean isCIAgree; + + //CI, 연계 정보 + @JsonProperty("ci") + public String ci; + + //CI 발급 시각, UTC + @JsonProperty("ci_authenticated_at") + public Date ciCreatedAt; + + @Getter + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public class Profile { + + //닉네임 + @JsonProperty("nickname") + public String nickName; + + //프로필 미리보기 이미지 URL + @JsonProperty("thumbnail_image_url") + public String thumbnailImageUrl; + + //프로필 사진 URL + @JsonProperty("profile_image_url") + public String profileImageUrl; + + //프로필 사진 URL 기본 프로필인지 여부 + //true : 기본 프로필, false : 사용자 등록 + @JsonProperty("is_default_image") + public String isDefaultImage; + + //닉네임이 기본 닉네임인지 여부 + //true : 기본 닉네임, false : 사용자 등록 + @JsonProperty("is_default_nickname") + public Boolean isDefaultNickName; + + } + } + + @Getter + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public class Partner { + //고유 ID + @JsonProperty("uuid") + public String uuid; + } + +} diff --git a/src/main/java/stackpot/stackpot/web/dto/LikeRequestDTO.java b/src/main/java/stackpot/stackpot/web/dto/LikeRequestDTO.java new file mode 100644 index 00000000..e0172912 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/LikeRequestDTO.java @@ -0,0 +1,15 @@ +package stackpot.stackpot.web.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Builder +@Getter +@Setter +public class LikeRequestDTO { + private Long applicationId; + + @Builder.Default + private Boolean liked = false; +} diff --git a/src/main/java/stackpot/stackpot/web/dto/LikedApplicantResponseDTO.java b/src/main/java/stackpot/stackpot/web/dto/LikedApplicantResponseDTO.java new file mode 100644 index 00000000..9ee52922 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/LikedApplicantResponseDTO.java @@ -0,0 +1,14 @@ +package stackpot.stackpot.web.dto; + +import lombok.Builder; +import lombok.Getter; +import stackpot.stackpot.domain.enums.Role; + +@Getter +@Builder +public class LikedApplicantResponseDTO { + private Long applicationId; + private Role applicantRole; + private String potNickname; // user의 nickname + pot_role 조합 + private Boolean liked; +} diff --git a/src/main/java/stackpot/stackpot/web/dto/MyPotResponseDTO.java b/src/main/java/stackpot/stackpot/web/dto/MyPotResponseDTO.java new file mode 100644 index 00000000..0d80ec22 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/MyPotResponseDTO.java @@ -0,0 +1,23 @@ +package stackpot.stackpot.web.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class MyPotResponseDTO { + + @JsonProperty("MyPots") + private List ongoingPots; + + @Getter + @Builder + public static class OngoingPotsDetail { + private UserResponseDto user; + private PotResponseDto pot; + private List potMembers; + } +} diff --git a/src/main/java/stackpot/stackpot/web/dto/MyPotTaskRequestDto.java b/src/main/java/stackpot/stackpot/web/dto/MyPotTaskRequestDto.java new file mode 100644 index 00000000..96d29ca1 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/MyPotTaskRequestDto.java @@ -0,0 +1,24 @@ +package stackpot.stackpot.web.dto; + +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import stackpot.stackpot.domain.enums.TaskboardStatus; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +public class MyPotTaskRequestDto { + @Data + @Getter + @NoArgsConstructor + public static class create{ + private String title; + private LocalDateTime deadline; + private TaskboardStatus taskboardStatus; + private String description; + private List participants; + } + +} diff --git a/src/main/java/stackpot/stackpot/web/dto/MyPotTaskResponseDto.java b/src/main/java/stackpot/stackpot/web/dto/MyPotTaskResponseDto.java new file mode 100644 index 00000000..2d22d6e1 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/MyPotTaskResponseDto.java @@ -0,0 +1,37 @@ +package stackpot.stackpot.web.dto; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.Pot; +import stackpot.stackpot.domain.User; +import stackpot.stackpot.domain.enums.TaskboardStatus; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@Builder +@Getter +@Setter +public class MyPotTaskResponseDto { + private Long taskboardId; + private String title; + private String description; + private TaskboardStatus status; + private LocalDateTime endDate; + private Long potId; + private List participants; + + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Participant { + private Long userId; + private Long potMemberId; + private String nickName; + } + +} diff --git a/src/main/java/stackpot/stackpot/web/dto/MyPotTodoRequestDTO.java b/src/main/java/stackpot/stackpot/web/dto/MyPotTodoRequestDTO.java new file mode 100644 index 00000000..9c031526 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/MyPotTodoRequestDTO.java @@ -0,0 +1,16 @@ +package stackpot.stackpot.web.dto; + +import jakarta.persistence.*; +import lombok.*; +import stackpot.stackpot.domain.enums.TodoStatus; + + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MyPotTodoRequestDTO { + private String content; + private TodoStatus status; +} diff --git a/src/main/java/stackpot/stackpot/web/dto/MyPotTodoResponseDTO.java b/src/main/java/stackpot/stackpot/web/dto/MyPotTodoResponseDTO.java new file mode 100644 index 00000000..0ba3d7b2 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/MyPotTodoResponseDTO.java @@ -0,0 +1,30 @@ +package stackpot.stackpot.web.dto; + +import lombok.*; +import stackpot.stackpot.domain.enums.TodoStatus; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MyPotTodoResponseDTO { + + private String userNickname; + private Long userId; + private List todos; + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class TodoDetailDTO { + private Long todoId; + private String content; + private TodoStatus status; + } +} + + diff --git a/src/main/java/stackpot/stackpot/web/dto/MyPotTodoUpdateRequestDTO.java b/src/main/java/stackpot/stackpot/web/dto/MyPotTodoUpdateRequestDTO.java new file mode 100644 index 00000000..b5da86f4 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/MyPotTodoUpdateRequestDTO.java @@ -0,0 +1,14 @@ +package stackpot.stackpot.web.dto; + +import lombok.*; + + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MyPotTodoUpdateRequestDTO { + private Long todoId; + private String content; +} diff --git a/src/main/java/stackpot/stackpot/web/dto/PotAllMemRequestDto.java b/src/main/java/stackpot/stackpot/web/dto/PotAllMemRequestDto.java new file mode 100644 index 00000000..83d5c8a6 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/PotAllMemRequestDto.java @@ -0,0 +1,22 @@ +package stackpot.stackpot.web.dto; + +import jakarta.validation.constraints.Pattern; +import lombok.*; +import stackpot.stackpot.Validation.annotation.ValidRole; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PotAllMemRequestDto { + private Long potMemberId; // 팟 멤버 ID + private Long potId; // 팟 ID + private Long userId; // 유저 ID + @ValidRole + private String roleName; // 역할 이름 + private String nickname; // 닉네임 + 역할 + private Boolean isOwner; // 팟 생성자인지 여부 + private String appealContent; // 어필 내용 +} + diff --git a/src/main/java/stackpot/stackpot/web/dto/PotAllResponseDTO.java b/src/main/java/stackpot/stackpot/web/dto/PotAllResponseDTO.java new file mode 100644 index 00000000..cb22f717 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/PotAllResponseDTO.java @@ -0,0 +1,27 @@ +package stackpot.stackpot.web.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class PotAllResponseDTO { + @JsonProperty("recruitingPots") + private List recruitingPots; + + @JsonProperty("ongoingPots") + private List ongoingPots; + + @JsonProperty("completedPots") + private List completedPots; + + @Getter + @Builder + public static class PotDetail { + private UserResponseDto user; + private PotResponseDto pot; + } +} diff --git a/src/main/java/stackpot/stackpot/web/dto/PotApplicationRequestDto.java b/src/main/java/stackpot/stackpot/web/dto/PotApplicationRequestDto.java new file mode 100644 index 00000000..9837cc99 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/PotApplicationRequestDto.java @@ -0,0 +1,20 @@ +package stackpot.stackpot.web.dto; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.*; +import stackpot.stackpot.Validation.annotation.ValidRole; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PotApplicationRequestDto { + @NotBlank(message = "팟 역할은 필수입니다.") + @ValidRole + private String potRole; + + +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/web/dto/PotApplicationResponseDto.java b/src/main/java/stackpot/stackpot/web/dto/PotApplicationResponseDto.java new file mode 100644 index 00000000..a4551714 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/PotApplicationResponseDto.java @@ -0,0 +1,22 @@ +package stackpot.stackpot.web.dto; + +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PotApplicationResponseDto { + private Long applicationId; + private String potRole; + private Boolean liked; + private String status; + private LocalDateTime appliedAt; + private Long potId; + private Long userId; + private String userNickname; +} + diff --git a/src/main/java/stackpot/stackpot/web/dto/PotMemberAppealResponseDto.java b/src/main/java/stackpot/stackpot/web/dto/PotMemberAppealResponseDto.java new file mode 100644 index 00000000..0b53a56f --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/PotMemberAppealResponseDto.java @@ -0,0 +1,18 @@ +package stackpot.stackpot.web.dto; + +import lombok.Builder; +import lombok.Getter; +import stackpot.stackpot.domain.enums.Role; + +@Getter +@Builder +public class PotMemberAppealResponseDto { + + private Long potMemberId; + private Long potId; + private Long userId; + private String roleName; + private Boolean isOwner; // 팟 생성자인지 여부 + private String nickname; // 닉네임 + 역할 + private String appealContent; +} diff --git a/src/main/java/stackpot/stackpot/web/dto/PotMemberRequestDto.java b/src/main/java/stackpot/stackpot/web/dto/PotMemberRequestDto.java new file mode 100644 index 00000000..02989216 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/PotMemberRequestDto.java @@ -0,0 +1,20 @@ +package stackpot.stackpot.web.dto; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class PotMemberRequestDto { + @ArraySchema( + arraySchema = @Schema( + description = "추가할 지원자 ID 리스트", + example = "[1, 2, 3]" + ) + ) + private List applicantIds; // 지원자 ID 리스트 +} diff --git a/src/main/java/stackpot/stackpot/web/dto/PotMemberResponseDTO.java b/src/main/java/stackpot/stackpot/web/dto/PotMemberResponseDTO.java new file mode 100644 index 00000000..e359fab3 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/PotMemberResponseDTO.java @@ -0,0 +1,15 @@ +package stackpot.stackpot.web.dto; + +import lombok.Builder; +import lombok.Getter; +import stackpot.stackpot.domain.enums.Role; + +@Getter +@Builder +public class PotMemberResponseDTO { + + private Long potMemberId; + private Role roleName; + private Boolean owner; + private String appealContent; +} diff --git a/src/main/java/stackpot/stackpot/web/dto/PotRecruitmentRequestDto.java b/src/main/java/stackpot/stackpot/web/dto/PotRecruitmentRequestDto.java new file mode 100644 index 00000000..5c5af7a6 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/PotRecruitmentRequestDto.java @@ -0,0 +1,16 @@ +package stackpot.stackpot.web.dto; + +import jakarta.validation.constraints.Pattern; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import stackpot.stackpot.Validation.annotation.ValidRole; + +@Getter +@Setter +@Builder +public class PotRecruitmentRequestDto { + @ValidRole + private String recruitmentRole; + private Integer recruitmentCount; +} diff --git a/src/main/java/stackpot/stackpot/web/dto/PotRecruitmentResponseDto.java b/src/main/java/stackpot/stackpot/web/dto/PotRecruitmentResponseDto.java new file mode 100644 index 00000000..1f463670 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/PotRecruitmentResponseDto.java @@ -0,0 +1,17 @@ +package stackpot.stackpot.web.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import stackpot.stackpot.domain.enums.Role; + +@Getter +@Setter +@Builder +public class PotRecruitmentResponseDto { + private Long recruitmentId; + private String recruitmentRole; + private Integer recruitmentCount; + +} diff --git a/src/main/java/stackpot/stackpot/web/dto/PotRequestDto.java b/src/main/java/stackpot/stackpot/web/dto/PotRequestDto.java new file mode 100644 index 00000000..04bbe638 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/PotRequestDto.java @@ -0,0 +1,41 @@ +package stackpot.stackpot.web.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.List; + +@Getter +@Setter +@Builder +public class PotRequestDto { + + @NotBlank(message = "팟 이름은 필수입니다.") + private String potName; + + private LocalDate potStartDate; + + private LocalDate potEndDate; + + @NotBlank(message = "예상 기간은 필수입니다.") + private String potDuration; + + @NotBlank(message = "사용 언어는 필수입니다.") + private String potLan; + + private String potContent; + + @NotBlank(message = "팟 상태는 필수입니다.") + private String potStatus; + + private String potModeOfOperation; + + private String potSummary; + + private LocalDate recruitmentDeadline; + + private List recruitmentDetails; +} diff --git a/src/main/java/stackpot/stackpot/web/dto/PotResponseDto.java b/src/main/java/stackpot/stackpot/web/dto/PotResponseDto.java new file mode 100644 index 00000000..93607982 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/PotResponseDto.java @@ -0,0 +1,27 @@ +package stackpot.stackpot.web.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import stackpot.stackpot.domain.Pot; + +import java.time.LocalDate; +import java.util.List; + +@Getter +@Setter +@Builder +public class PotResponseDto { + private Long potId; + private String potName; + private String potStartDate; + private String potEndDate; + private String potDuration; + private String potLan; + private String potContent; + private String potStatus; + private String potModeOfOperation; + private String potSummary; + private LocalDate recruitmentDeadline; + private List recruitmentDetails; +} diff --git a/src/main/java/stackpot/stackpot/web/dto/PotSearchResponseDto.java b/src/main/java/stackpot/stackpot/web/dto/PotSearchResponseDto.java new file mode 100644 index 00000000..9d2ae5b6 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/PotSearchResponseDto.java @@ -0,0 +1,20 @@ +package stackpot.stackpot.web.dto; + +import lombok.*; + +import java.time.LocalDate; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PotSearchResponseDto { + private Long potId; + private String potName; + private String potContent; + private String creatorNickname; + private String creatorRole; + private String recruitmentPart; + private LocalDate recruitmentDeadline; +} diff --git a/src/main/java/stackpot/stackpot/web/dto/PotSummaryResponseDTO.java b/src/main/java/stackpot/stackpot/web/dto/PotSummaryResponseDTO.java new file mode 100644 index 00000000..77cb5f3b --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/PotSummaryResponseDTO.java @@ -0,0 +1,10 @@ +package stackpot.stackpot.web.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class PotSummaryResponseDTO { + private String summary; +} diff --git a/src/main/java/stackpot/stackpot/web/dto/RecruitmentDetailResponseDto.java b/src/main/java/stackpot/stackpot/web/dto/RecruitmentDetailResponseDto.java new file mode 100644 index 00000000..7b1c26d4 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/RecruitmentDetailResponseDto.java @@ -0,0 +1,14 @@ +package stackpot.stackpot.web.dto; + +import lombok.*; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class RecruitmentDetailResponseDto { + private String recruitmentRole; + private Integer recruitmentCount; +} + diff --git a/src/main/java/stackpot/stackpot/web/dto/RecruitmentDetailsResponseDTO.java b/src/main/java/stackpot/stackpot/web/dto/RecruitmentDetailsResponseDTO.java new file mode 100644 index 00000000..d9c930af --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/RecruitmentDetailsResponseDTO.java @@ -0,0 +1,16 @@ +package stackpot.stackpot.web.dto; + + +import lombok.Builder; +import lombok.Getter; +import stackpot.stackpot.domain.enums.Role; + +@Getter +@Builder +public class RecruitmentDetailsResponseDTO { + + private Long recruitmentId; + private Role recruitmentRole; + private Integer recruitmentCount; + +} diff --git a/src/main/java/stackpot/stackpot/web/dto/TokenServiceResponse.java b/src/main/java/stackpot/stackpot/web/dto/TokenServiceResponse.java new file mode 100644 index 00000000..76bbadf3 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/TokenServiceResponse.java @@ -0,0 +1,30 @@ +package stackpot.stackpot.web.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; + +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TokenServiceResponse { + + private final String accessToken; + private final String refreshToken; + + public TokenServiceResponse(String accessToken) { + this.accessToken = accessToken; + this.refreshToken = null; + } + + public TokenServiceResponse(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } + + public static TokenServiceResponse of(final String accessToken, final String refreshToken) { + return new TokenServiceResponse(accessToken, refreshToken); + } + + public TokenServiceResponse withoutRefreshToken() { + return new TokenServiceResponse(this.accessToken); + } +} \ No newline at end of file diff --git a/src/main/java/stackpot/stackpot/web/dto/UpdateAppealRequestDto.java b/src/main/java/stackpot/stackpot/web/dto/UpdateAppealRequestDto.java new file mode 100644 index 00000000..23967ff0 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/UpdateAppealRequestDto.java @@ -0,0 +1,13 @@ +package stackpot.stackpot.web.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UpdateAppealRequestDto { + + private String appealContent; +} + diff --git a/src/main/java/stackpot/stackpot/web/dto/UserMypageResponseDto.java b/src/main/java/stackpot/stackpot/web/dto/UserMypageResponseDto.java new file mode 100644 index 00000000..4ea8ba4d --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/UserMypageResponseDto.java @@ -0,0 +1,52 @@ +package stackpot.stackpot.web.dto; + +import lombok.*; +import stackpot.stackpot.domain.enums.Category; +import stackpot.stackpot.domain.enums.Role; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class UserMypageResponseDto { + private Long id; + private String email; + private String nickname; + private Role role; + private String interest; + private Integer userTemperature; + private String kakaoId; + private String userIntroduction; + private List completedPots; + private List feeds; + + @Getter + @Setter + @Builder + public static class CompletedPotDto { + private Long potId; + private String potName; + private String potStartDate; + private String potEndDate; + private String potSummary; + private List recruitmentDetails; + } + + @Getter + @Setter + @Builder + public static class FeedDto { + private Long feedId; + private String title; + private String content; + private Category category; + private Long likeCount; + private String createdAt; + } +} + diff --git a/src/main/java/stackpot/stackpot/web/dto/UserRequestDto.java b/src/main/java/stackpot/stackpot/web/dto/UserRequestDto.java new file mode 100644 index 00000000..2b7e085c --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/UserRequestDto.java @@ -0,0 +1,27 @@ +package stackpot.stackpot.web.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import stackpot.stackpot.domain.enums.Role; + +public class UserRequestDto { + + @Getter + @Setter + @NoArgsConstructor + public static class JoinDto { + + Role role; + @NotBlank(message = "Interest는 공백일 수 없습니다.") + String interest; + @NotBlank(message = "Nickname은 공백일 수 없습니다.") + String nickname; + @NotBlank(message = "KakaoId는 공백일 수 없습니다.") + String kakaoId; + } +} diff --git a/src/main/java/stackpot/stackpot/web/dto/UserResponseDto.java b/src/main/java/stackpot/stackpot/web/dto/UserResponseDto.java new file mode 100644 index 00000000..17d09440 --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/UserResponseDto.java @@ -0,0 +1,21 @@ +package stackpot.stackpot.web.dto; + +import jakarta.persistence.Column; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import stackpot.stackpot.domain.enums.Role; + +@Getter +@Setter +@Builder +public class UserResponseDto { + private Long id; + private String email; // 이메일 + private String nickname; // 닉네임 + private Role role; // 역할 + private String interest; // 관심사 + private Integer userTemperature; // 유저 온도 + private String kakaoId; + private String userIntroduction; +} diff --git a/src/main/java/stackpot/stackpot/web/dto/UserUpdateRequestDto.java b/src/main/java/stackpot/stackpot/web/dto/UserUpdateRequestDto.java new file mode 100644 index 00000000..17332a3b --- /dev/null +++ b/src/main/java/stackpot/stackpot/web/dto/UserUpdateRequestDto.java @@ -0,0 +1,17 @@ +package stackpot.stackpot.web.dto; + +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.Setter; +import stackpot.stackpot.Validation.annotation.ValidRole; +import stackpot.stackpot.domain.enums.Role; + + +@Getter +@Setter +public class UserUpdateRequestDto { + @ValidRole + private Role role; + private String interest; + private String userIntroduction; +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 40cd959d..00000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=stackpot diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 00000000..ae5a4a85 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,51 @@ +spring: + config: + import: optional:file:.env[.properties] + datasource: + url: ${URL} + username: ${USERNAME} + password: ${PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + sql: + init: + mode: never + jpa: + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect + show_sql: true + format_sql: true + use_sql_comments: true + hbm2ddl: + auto: update + default_batch_fetch_size: 1000 + cloud: + aws: + s3: + bucket: stackpot + path: + FeedFile: FeedFile + region: + static: ap-northeast-2 + stack: + auto: false + credentials: + accessKey: ${AWS_ACCESS_KEY_ID} + secretKey: ${AWS_SECRET_ACCESS_KEY} + jwt: + secret: ${JWT_SECRET} + mail: + host: smtp.gmail.com + port: 587 + username: ${mail.username} + password: ${mail.password} + properties: + mail: + smtp: + auth: true + timeout: 5000 + starttls: + enable: true + task: + scheduling: + enabled: true \ No newline at end of file diff --git a/src/test/java/stackpot/stackpot/controller/MemberViewControllerTest.java b/src/test/java/stackpot/stackpot/controller/MemberViewControllerTest.java new file mode 100644 index 00000000..2d9757d8 --- /dev/null +++ b/src/test/java/stackpot/stackpot/controller/MemberViewControllerTest.java @@ -0,0 +1,51 @@ +//package stackpot.stackpot.controller; +// +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +//import org.springframework.boot.test.mock.mockito.MockBean; +//import org.springframework.test.web.servlet.MockMvc; +//import stackpot.stackpot.service.UserCommandService; +//import stackpot.stackpot.web.controller.MemberViewController; +//import stackpot.stackpot.web.dto.UserRequestDTO; +// +//import static org.mockito.Mockito.*; +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +// +//@WebMvcTest(MemberViewController.class) +//class MemberViewControllerTest { +// +// @Autowired +// private MockMvc mockMvc; +// +// @MockBean +// private UserCommandService userCommandService; +// +// @Test +// void testJoinUser_Success() throws Exception { +// // Mock request data +// UserRequestDTO.JoinDto joinDto = new UserRequestDTO.JoinDto(); +// joinDto.setEmail("test@example.com"); +// joinDto.setNickname("TestUser"); +// joinDto.setKakaoId("12345"); +// +// doNothing().when(userCommandService).joinUser(any(UserRequestDTO.JoinDto.class)); +// +// mockMvc.perform(post("/user/profile") +// .flashAttr("UserJoinDto", joinDto)) +// .andExpect(status().is3xxRedirection()) +// .andExpect(redirectedUrl("/login")); +// } +// +// @Test +// void testJoinUser_ValidationError() throws Exception { +// mockMvc.perform(post("/user/profile") +// .param("email", "") // 잘못된 데이터 +// .param("nickname", "") +// .param("kakaoId", "")) +// .andExpect(status().isOk()) +// .andExpect(view().name("signup")) +// .andExpect(model().attributeExists("error")); +// } +//} \ No newline at end of file