diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d7cd14..20ac53a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,10 +15,10 @@ jobs: - name: Grant execute permission to gradlew run: chmod +x ./gradlew - - - name: Create .env file from GitHub Secrets - run: | - echo "${{ secrets.ENV_FILE }}" > .env + + - name: Extract commit hash + id: vars + run: echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - name: DockerHub 로그인 uses: docker/login-action@v2 @@ -28,17 +28,6 @@ jobs: - name: Docker 이미지 빌드 및 푸시 run: | - docker build -t ${{ secrets.DOCKER_USERNAME }}/dododocs-be:latest . - docker push ${{ secrets.DOCKER_USERNAME }}/dododocs-be:latest - - - name: 컨테이너 실행 및 테스트 - run: | - docker run -d -p 8080:8080 --env-file .env --rm ${{ secrets.DOCKER_USERNAME }}/dododocs-be:latest & - sleep 20 # 애플리케이션이 시작될 시간을 줍니다. - # 기본 경로에서 서버 확인 - HTTP_STATUS=$(curl -o /dev/null -s -w "%{http_code}" http://localhost:8080/) - if [ "$HTTP_STATUS" -ne 200 ] && [ "$HTTP_STATUS" -ne 404 ]; then - echo "Unexpected HTTP status: $HTTP_STATUS" - exit 1 - fi - echo "Server is running with HTTP status: $HTTP_STATUS" \ No newline at end of file + echo "Using commit hash: ${{ env.COMMIT_HASH }}" + docker build -t ${{ secrets.DOCKER_USERNAME }}/dododocs-be:${{ env.COMMIT_HASH }} . + docker push ${{ secrets.DOCKER_USERNAME }}/dododocs-be:${{ env.COMMIT_HASH }} \ No newline at end of file diff --git a/.github/workflows/fork_for_test.yml b/.github/workflows/fork_for_test.yml index d0a4189..19a7459 100644 --- a/.github/workflows/fork_for_test.yml +++ b/.github/workflows/fork_for_test.yml @@ -18,7 +18,12 @@ jobs: - name: Set up Git user run: | git config --global user.name "42kko_syncbot" - + + # Dockerfile에서 주석 제거 + - name: Uncomment proxy settings in Dockerfile + run: | + sed -i '/# RUN echo "systemProp.http.proxyHost=krmp-proxy.9rum.cc/ s/^# //' Dockerfile + # .github/workflows 디렉토리 삭제 - name: Remove workflows directory run: | diff --git a/Dockerfile b/Dockerfile index dbe810e..898c035 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,3 @@ -# gradle:7.3.1-jdk17 이미지를 기반으로 함 FROM gradle:8.5.0-jdk21 # 작업 디렉토리 설정 @@ -8,11 +7,11 @@ WORKDIR /home/gradle/project COPY . . # gradle 빌드 시 proxy 설정을 gradle.properties에 추가 -RUN echo "systemProp.http.proxyHost=krmp-proxy.9rum.cc\nsystemProp.http.proxyPort=3128\nsystemProp.https.proxyHost=krmp-proxy.9rum.cc\nsystemProp.https.proxyPort=3128" > /root/.gradle/gradle.properties +# RUN echo "systemProp.http.proxyHost=krmp-proxy.9rum.cc\nsystemProp.http.proxyPort=3128\nsystemProp.https.proxyHost=krmp-proxy.9rum.cc\nsystemProp.https.proxyPort=3128" > /root/.gradle/gradle.properties # gradlew를 이용한 프로젝트 필드 RUN ./gradlew clean build # 빌드 결과 jar 파일을 실행 #CMD ["java", "-jar", "/home/gradle/project/build/libs/dododocs-0.0.1-SNAPSHOT.jar"] -CMD ["sh", "-c", "env && exec java -jar /home/gradle/project/build/libs/dododocs-0.0.1-SNAPSHOT.jar"] +CMD ["sh", "-c", "env && exec java -jar -Dspring.profiles.active=dev /home/gradle/project/build/libs/dododocs-0.0.1-SNAPSHOT.jar"] diff --git a/build.gradle b/build.gradle index 602b259..84697b2 100644 --- a/build.gradle +++ b/build.gradle @@ -54,6 +54,10 @@ dependencies { testImplementation 'io.rest-assured:rest-assured' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' + + implementation 'org.springframework.boot:spring-boot-starter-webflux' + + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.563' } test { diff --git a/src/docs/asciidoc/analyze.adoc b/src/docs/asciidoc/analyze.adoc new file mode 100644 index 0000000..6d900bd --- /dev/null +++ b/src/docs/asciidoc/analyze.adoc @@ -0,0 +1,160 @@ +== ⛳️ AI 문서화 +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 2 + +=== AI 분석 및 문서화 요청 (레포 등록) + +include::{snippets}/analyze/upload/success/http-request.adoc[] + +==== HTTP Request + +include::{snippets}/analyze/upload/success/http-request.adoc[] + +==== Request Body + +include::{snippets}/analyze/upload/success/request-fields.adoc[] + +==== HTTP Response + +include::{snippets}/analyze/upload/success/http-response.adoc[] + +=== 레포지토리 등록 실패 (존재하지 않는 (잘못된) 깃허브 레포지토리 URL 이나 브랜치명을 입력 받았을 때) + +==== HTTP Request + +include::{snippets}/analyze/upload/fail/noExistRepoInfo/http-request.adoc[] + +===== Request Body + +include::{snippets}/analyze/upload/fail/noExistRepoInfo/request-fields.adoc[] + +==== HTTP Response + +include::{snippets}/analyze/upload/fail/noExistRepoInfo/http-response.adoc[] + +=== AI 분석 DOCS 결과 불러오기 + +==== HTTP Request + +include::{snippets}/analyze/download/docs/success/http-request.adoc[] + +==== Path Variable + +include::{snippets}/analyze/download/docs/success/path-parameters.adoc[] + +==== HTTP Response + +include::{snippets}/analyze/download/docs/success/http-response.adoc[] + +===== Response Body + +include::{snippets}/analyze/download/docs/success/response-fields.adoc[] + +=== AI 분석 README 결과 불러오기 + +==== HTTP Request + +include::{snippets}/analyze/download/readme/success/http-request.adoc[] + +==== Path Variable + +include::{snippets}/analyze/download/readme/success/path-parameters.adoc[] + +==== HTTP Response + +include::{snippets}/analyze/download/readme/success/http-response.adoc[] + +===== Response Body + +include::{snippets}/analyze/download/readme/success/response-fields.adoc[] + + +=== AI 분석 DOCS (또는 README, 챗봇 질문하기 등) 결과 불러오기 실패 (아직 AI 가 Document 생성을 완료하지 못한 경우) + +==== HTTP Request + +include::{snippets}/analyze/download/docs/fail/http-request.adoc[] + +==== Path Variable + +include::{snippets}/analyze/download/docs/fail/path-parameters.adoc[] + +==== HTTP Response + +include::{snippets}/analyze/download/docs/fail/http-response.adoc[] + +=== 멤버가 등록한 레포지토리 리스트 조회 + +==== HTTP Request + +include::{snippets}/repos/registered/find/success/http-request.adoc[] + +==== Request Header + +include::{snippets}/repos/registered/find/success/request-headers.adoc[] + +==== HTTP Response + +include::{snippets}/repos/registered/find/success/http-response.adoc[] + +==== Response Field + +include::{snippets}/repos/registered/find/success/response-fields.adoc[] + +=== 등록된 레포지토리 삭제 + +==== HTTP Request + +include::{snippets}/register/delete/success/http-request.adoc[] + +===== Request Header + +include::{snippets}/register/delete/success/request-headers.adoc[] + +===== Request Body + +include::{snippets}/register/delete/success/request-fields.adoc[] + +==== HTTP Response + +include::{snippets}/register/delete/success/http-response.adoc[] + + +=== 레포에서 특정 파일명을 입력했을 때, 그에 대한 리드미 내용을 제공하는 API + +==== HTTP Request + +include::{snippets}/download/repo/file/detail/success/http-request.adoc[] + +===== Request Header + +include::{snippets}/download/repo/file/detail/success/request-headers.adoc[] + +===== Query Param + +include::{snippets}/download/repo/file/detail/success/query-parameters.adoc[] + +==== HTTP Response + +include::{snippets}/download/repo/file/detail/success/http-response.adoc[] + +===== Response Body + +include::{snippets}/download/repo/file/detail/success/response-fields.adoc[] + +=== 리드미 내용 수정 API + +==== HTTP Request + +include::{snippets}/readme/update/success/http-request.adoc[] + +===== Request Param + +include::{snippets}/readme/update/success/query-parameters.adoc[] + +==== HTTP Response + +include::{snippets}/readme/update/success/http-response.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc index ed53078..1c6a4b5 100644 --- a/src/docs/asciidoc/auth.adoc +++ b/src/docs/asciidoc/auth.adoc @@ -38,3 +38,18 @@ include::{snippets}/auth/generate/token/success/http-response.adoc[] ===== Response Body include::{snippets}/auth/generate/token/success/response-fields.adoc[] + + +=== 토큰 만료되었을 때 (로그아웃 해야할 때) + +==== HTTP Request + +include::{snippets}/auth/logout/success/http-request.adoc[] + +==== Request Header + +include::{snippets}/auth/logout/success/request-headers.adoc[] + +==== HTTP Response + +include::{snippets}/auth/logout/success/http-response.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/chatbot.adoc b/src/docs/asciidoc/chatbot.adoc new file mode 100644 index 0000000..1ef2726 --- /dev/null +++ b/src/docs/asciidoc/chatbot.adoc @@ -0,0 +1,44 @@ +== ⛳️ Chatbot (챗봇) +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 2 + +=== 챗봇 대화 내역 불러오기 + +==== HTTP Request + +include::{snippets}/chatbot/find/logs/success/http-request.adoc[] + +==== Request Header + +include::{snippets}/chatbot/find/logs/success/request-headers.adoc[] + +==== HTTP Response + +include::{snippets}/chatbot/find/logs/success/http-response.adoc[] + + +=== 챗봇에게 질문하기 + +==== HTTP Request + +include::{snippets}/chatbot/ask/success/http-request.adoc[] + +===== Request Header + +include::{snippets}/chatbot/ask/success/request-headers.adoc[] + +===== Request Body + +include::{snippets}/chatbot/ask/success/request-fields.adoc[] + + +==== HTTP Response + +include::{snippets}/chatbot/ask/success/http-response.adoc[] + +==== Response Body + +include::{snippets}/chatbot/ask/success/response-fields.adoc[] diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index ed427c7..0f80b4f 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -6,4 +6,7 @@ :toclevels: 2 include::auth.adoc[] -include::member.adoc[] \ No newline at end of file +include::member.adoc[] +include::analyze.adoc[] +include::chatbot.adoc[] +include::test.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/member.adoc b/src/docs/asciidoc/member.adoc index 01b4161..f442597 100644 --- a/src/docs/asciidoc/member.adoc +++ b/src/docs/asciidoc/member.adoc @@ -25,4 +25,22 @@ include::{snippets}/member/repos/http-response.adoc[] ===== Response Body -include::{snippets}/member/repos/response-fields.adoc[] \ No newline at end of file +include::{snippets}/member/repos/response-fields.adoc[] + +=== 멤버 기본 프로필 정보 조회 + +==== HTTP Request + +include::{snippets}/member/profile/success/http-request.adoc[] + +===== Request Header + +include::{snippets}/member/profile/success/request-headers.adoc[] + +==== HTTP Response + +include::{snippets}/member/profile/success/http-response.adoc[] + +===== Response Fields + +include::{snippets}/member/profile/success/response-fields.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/test.adoc b/src/docs/asciidoc/test.adoc new file mode 100644 index 0000000..78ad96e --- /dev/null +++ b/src/docs/asciidoc/test.adoc @@ -0,0 +1,31 @@ +== ⛳️ Test (테스트 API) +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 2 + +=== AI README 분석 결과 조회 테스트 API + +==== HTTP Request + +include::{snippets}/test/analyze/result/success/http-request.adoc[] + +==== HTTP Response + +include::{snippets}/test/analyze/result/success/http-response.adoc[] + +=== 리드미 수정 테스트 API + +==== HTTP Request + +include::{snippets}/test/readme/update/success/http-request.adoc[] + +===== Request Param + +include::{snippets}/test/readme/update/success/query-parameters.adoc[] + +==== HTTP Response + +include::{snippets}/readme/update/success/http-response.adoc[] + diff --git a/src/main/java/dododocs/dododocs/analyze/application/AnalyzeService.java b/src/main/java/dododocs/dododocs/analyze/application/AnalyzeService.java index c91be94..973c7ca 100644 --- a/src/main/java/dododocs/dododocs/analyze/application/AnalyzeService.java +++ b/src/main/java/dododocs/dododocs/analyze/application/AnalyzeService.java @@ -1,56 +1,324 @@ package dododocs.dododocs.analyze.application; +import aj.org.objectweb.asm.TypeReference; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.fasterxml.jackson.databind.ObjectMapper; +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import dododocs.dododocs.analyze.domain.repository.MemberOrganizationRepository; +import dododocs.dododocs.analyze.domain.repository.RepoAnalyzeRepository; +import dododocs.dododocs.analyze.dto.ExternalAiZipAnalyzeRequest; +import dododocs.dododocs.analyze.dto.ExternalAiZipAnalyzeResponse; +import dododocs.dododocs.analyze.dto.RepositoryContentDto; +import dododocs.dododocs.analyze.dto.UploadGitRepoContentToS3Request; +import dododocs.dododocs.analyze.exception.MaxSizeRepoRegiserException; +import dododocs.dododocs.analyze.exception.NoExistGitRepoException; +import dododocs.dododocs.analyze.infrastructure.ExternalAiZipAnalyzeClient; +import dododocs.dododocs.auth.domain.repository.MemberRepository; +import dododocs.dododocs.auth.exception.NoExistMemberException; +import dododocs.dododocs.chatbot.application.ChatbotService; +import dododocs.dododocs.chatbot.infrastructure.ExternalChatbotClient; +import dododocs.dododocs.member.domain.Member; import org.springframework.core.io.InputStreamResource; -import org.springframework.http.HttpHeaders; +import org.springframework.http.*; import org.springframework.stereotype.Service; -import java.io.InputStream; +import java.io.*; +import java.net.InetSocketAddress; +import java.net.Proxy; import java.net.URL; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.ObjectMetadata; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; +import java.net.URLConnection; +import java.util.*; import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; - +@RequiredArgsConstructor @Service public class AnalyzeService { - private final String bucketName = "haon-dododocs"; // S3 버킷 이름 + private static final Integer REPO_REGISTER_MAX_SIZE = 3; + private final ExternalAiZipAnalyzeClient externalAiZipAnalyzeClient; + private final MemberRepository memberRepository; + private final AmazonS3Client amazonS3Client; + private final MemberOrganizationRepository memberOrganizationRepository; + private final RepoAnalyzeRepository repoAnalyzeRepository; + // GitHub 레포지토리를 ZIP 파일로 가져와 S3에 업로드 + public void uploadGithubRepoToS3(final UploadGitRepoContentToS3Request uploadGitRepoContentToS3Request, final long memberId) { - public void uploadZipToS3(String fileName) { - try { - // 1. ZIP 파일 생성 - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - try (ZipOutputStream zipOut = new ZipOutputStream(byteArrayOutputStream)) { - ZipEntry zipEntry = new ZipEntry("example.txt"); // 압축할 파일명 지정 - zipOut.putNextEntry(zipEntry); - zipOut.write("This is an example file content.".getBytes()); // 파일 내용 작성 - zipOut.closeEntry(); + final String repoName = uploadGitRepoContentToS3Request.getRepositoryName(); + final String branchName = uploadGitRepoContentToS3Request.getBranchName(); + + // Member를 조회 + final Member member = memberRepository.findById(memberId) + .orElseThrow(NoExistMemberException::new); + + final List repoAnalyzes = repoAnalyzeRepository.findByMember(member); + + if(repoAnalyzes.size() >= REPO_REGISTER_MAX_SIZE) { + throw new MaxSizeRepoRegiserException("최대 3개의 레포지토리를 등록 가능합니다."); + } + + // 개인 소유자로 먼저 시도 + String ownerName = member.getOriginName(); + + // docs_key : msung99_moheng_main_DOCS.zip + // readme_key : msung99_moheng_main_README.md + String docsKey = ownerName + "_" + repoName + "_" + branchName + "_DOCS.zip"; + String readmeKey = ownerName + "_" + repoName + "_" + branchName + "_README.md"; + + boolean success = tryUploadFromOwner( + member.getAccessToken(), + String.format("https://github.com/%s/%s/%s", ownerName, repoName, branchName), + uploadGitRepoContentToS3Request.isIncludeTest(), + docsKey, + readmeKey, + uploadGitRepoContentToS3Request.isKorean(), + ownerName, + repoName, + branchName); + + // 개인 소유에서 찾지 못하면 조직 소유로 검색 + if (!success) { + List organizationNames = findOrganizationNames(member); + for (String orgName : organizationNames) { + success = tryUploadFromOwner( + member.getAccessToken(), + String.format("https://github.com/%s/%s/%s", ownerName, repoName, branchName), + uploadGitRepoContentToS3Request.isIncludeTest(), + docsKey, + readmeKey, + uploadGitRepoContentToS3Request.isKorean(), + orgName, repoName, branchName); + if (success) { + break; + } } + } - // 2. S3에 업로드하기 위한 InputStream 생성 - byte[] zipBytes = byteArrayOutputStream.toByteArray(); - ByteArrayInputStream inputStream = new ByteArrayInputStream(zipBytes); + if(!success) { + throw new NoExistGitRepoException("존재하지 않는 레포지토리 또는 브랜치입니다."); + } - // 3. 메타데이터 설정 - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(zipBytes.length); - metadata.setContentType("application/zip"); + String s3Key = ownerName + "-" + repoName; + String repoUrl = String.format("https://github.com/%s/%s/%s", ownerName, repoName, branchName); + + repoAnalyzeRepository.save( + new RepoAnalyze(repoName, + branchName, + readmeKey, + docsKey, + repoUrl, + member) + ); + } - // 4. S3에 ZIP 파일 업로드 - // amazonS3Client.putObject(bucketName, fileName + ".zip", inputStream, metadata); + private boolean tryUploadFromOwner(String accessToken, String repoUrl, boolean includeTest, String docsKey, String readmeKey, boolean korean, + String ownerName, String repoName, String branchName) { - System.out.println("ZIP 파일 업로드 성공: " + fileName + ".zip"); + String downloadUrl = String.format("https://api.github.com/repos/%s/%s/zipball/%s", ownerName, repoName, branchName); + File tempFile = null; + try { + tempFile = File.createTempFile(repoName, ".zip"); + downloadFileFromUrlWithAuth(downloadUrl, accessToken, tempFile); } catch (Exception e) { - System.err.println("ZIP 파일 업로드 중 오류 발생: " + e.getMessage()); - e.printStackTrace(); + return false; + } + + try { + ObjectMetadata metadata = new ObjectMetadata(); + metadata.addUserMetadata("repo_url", repoUrl); + metadata.addUserMetadata("include_test", String.valueOf(includeTest)); + metadata.addUserMetadata("docs_key", docsKey); + metadata.addUserMetadata("readme_key", readmeKey); + metadata.addUserMetadata("korean", String.valueOf(korean)); + + amazonS3Client.putObject(new PutObjectRequest("haon-dododocs", "source/" + ownerName + "-" + repoName + "-" + branchName, tempFile).withMetadata(metadata)); + } catch (Exception e) { + return false; + } + + tempFile.delete(); + return true; + } + + private void downloadFileFromUrlWithAuth(String url, String accessToken, File destinationFile) throws IOException { + URLConnection connection = new URL(url).openConnection(); + + System.out.println("🌴 accessToken: " + accessToken); + + connection.setRequestProperty("Authorization", "Bearer " + accessToken); + try (InputStream inputStream = connection.getInputStream(); + OutputStream outputStream = new FileOutputStream(destinationFile)) { + + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } + } + + private List findOrganizationNames(final Member member) { + return memberOrganizationRepository.findOrganizationNamesByMember(member); + } + + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private final String GITHUB_API_BASE_URL = "https://api.github.com/repos"; + + public RepositoryContentDto getRepositoryContents(long memberId, String repo, String branch) throws IOException { + final Member member = memberRepository.findById(memberId) + .orElseThrow(NoExistMemberException::new); + + // 개인 소유자로 먼저 시도 + String ownerName = member.getOriginName(); + RepositoryContentDto result = tryGetRepositoryContents(ownerName, repo, branch); + + // 개인 소유에서 찾지 못하면 조직 소유로 검색 + if (result == null) { + List organizationNames = findOrganizationNames(member); + for (String orgName : organizationNames) { + result = tryGetRepositoryContents(orgName, repo, branch); + if (result != null) { + break; + } + } + } + + if (result == null) { + throw new IllegalArgumentException("Could not find repository " + repo + " under any owner."); + } + + return result; + } + + // 특정 소유자(개인 또는 조직)에서 레포지토리를 가져오기 시도 + private RepositoryContentDto tryGetRepositoryContents(String ownerName, String repo, String branch) { + try { + // GitHub ZIP 다운로드 URL + String zipUrl = String.format("https://github.com/%s/%s/archive/refs/heads/%s.zip", ownerName, repo, branch); + + // 1. ZIP 파일 다운로드 + File tempZipFile = File.createTempFile(repo, ".zip"); + downloadFile(zipUrl, tempZipFile); + + // 2. ZIP 파일 압축 해제 + File tempDir = new File(tempZipFile.getParent(), repo); + unzip(tempZipFile, tempDir); + + // 3. 폴더 및 파일 구조 생성 + RepositoryContentDto root = parseFolder(tempDir); + + // 4. 임시 파일 삭제 + tempZipFile.delete(); + deleteFolder(tempDir); + + return root; + } catch (IOException e) { + System.err.println("Failed to retrieve repository for owner: " + ownerName); + return null; + } + } + + // 파일 다운로드 + private void downloadFile(String url, File destinationFile) throws IOException { + URLConnection connection = new URL(url).openConnection(); + try (InputStream inputStream = connection.getInputStream(); + OutputStream outputStream = new FileOutputStream(destinationFile)) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } + } + + // ZIP 파일 압축 해제 + private void unzip(File zipFile, File destDir) throws IOException { + byte[] buffer = new byte[1024]; + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) { + ZipEntry zipEntry = zis.getNextEntry(); + while (zipEntry != null) { + File newFile = new File(destDir, zipEntry.getName()); + if (zipEntry.isDirectory()) { + newFile.mkdirs(); + } else { + new File(newFile.getParent()).mkdirs(); + try (FileOutputStream fos = new FileOutputStream(newFile)) { + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + } + } + zipEntry = zis.getNextEntry(); + } + zis.closeEntry(); + } + } + + // 폴더 및 파일 구조 생성 + private RepositoryContentDto parseFolder(File folder) throws IOException { + RepositoryContentDto dto = new RepositoryContentDto(); + dto.setName(folder.getName()); + dto.setType("directory"); + dto.setChildren(new ArrayList<>()); + + for (File file : folder.listFiles()) { + if (file.isDirectory()) { + dto.getChildren().add(parseFolder(file)); // 재귀적으로 하위 폴더 탐색 + } else { + RepositoryContentDto fileDto = new RepositoryContentDto(); + fileDto.setName(file.getName()); + fileDto.setType("file"); + fileDto.setContent(readFileContent(file)); // 파일 내용 읽기 + dto.getChildren().add(fileDto); + } + } + return dto; + } + + // 파일 내용 읽기 + private String readFileContent(File file) throws IOException { + if (file.getName().endsWith(".png") || file.getName().endsWith(".jpg")) { + // 바이너리 파일은 Base64로 인코딩 + byte[] fileBytes = java.nio.file.Files.readAllBytes(file.toPath()); + return Base64.getEncoder().encodeToString(fileBytes); + } else { + // 텍스트 파일은 그대로 읽기 + StringBuilder content = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new FileReader(file))) { + String line; + while ((line = br.readLine()) != null) { + content.append(line).append("\n"); + } + } + return content.toString(); + } + } + + // 폴더 삭제 + private void deleteFolder(File folder) { + File[] files = folder.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteFolder(file); + } else { + file.delete(); + } + } } + folder.delete(); } } diff --git a/src/main/java/dododocs/dododocs/analyze/application/DownloadFromS3Service.java b/src/main/java/dododocs/dododocs/analyze/application/DownloadFromS3Service.java new file mode 100644 index 0000000..edf4f7c --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/application/DownloadFromS3Service.java @@ -0,0 +1,340 @@ +package dododocs.dododocs.analyze.application; + +import com.amazonaws.services.s3.AmazonS3Client; +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import dododocs.dododocs.analyze.domain.repository.RepoAnalyzeRepository; +import dododocs.dododocs.analyze.dto.DownloadAiAnalyzeResponse; +import dododocs.dododocs.analyze.dto.DownloadReadmeAnalyzeResponse; +import dododocs.dododocs.analyze.dto.EmptyFolderException; +import dododocs.dododocs.analyze.exception.NoExistRepoAnalyzeException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.io.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + + +@RequiredArgsConstructor +@Service +public class DownloadFromS3Service { + private final RepoAnalyzeRepository repoAnalyzeRepository; + private final AmazonS3Client amazonS3Client; + private final String bucketName = "haon-dododocs"; + + public DownloadAiAnalyzeResponse downloadAndProcessZipDocsInfo(final long registeredRepoId) throws IOException { + final RepoAnalyze repoAnalyze = repoAnalyzeRepository.findById(registeredRepoId) + .orElseThrow(() -> new NoExistRepoAnalyzeException("레포지토리 정보가 존재하지 않습니다.")); + + final String s3Key = repoAnalyze.getDocsKey(); + + // 1. S3에서 ZIP 파일 다운로드 + File zipFile = downloadZipFromS3(bucketName, s3Key); + + // 2. ZIP 파일 압축 해제 + File extractedDir = unzipFile(zipFile); + + // 3. .md 파일을 FileDetail 형식으로 변환하여 분류 + Map> categorizedFiles = collectAndCategorizeMarkdownFiles(extractedDir); + + // 4. 임시 파일 삭제 + zipFile.delete(); + deleteDirectory(extractedDir); + + return new DownloadAiAnalyzeResponse(categorizedFiles.get("summary"), categorizedFiles.get("regular")); + } + + public DownloadAiAnalyzeResponse downloadAndProcessZipDocsInfoTest(final long registeredRepoId) throws IOException { + final RepoAnalyze repoAnalyze = repoAnalyzeRepository.findById(registeredRepoId) + .orElseThrow(() -> new NoExistRepoAnalyzeException("레포지토리 정보가 존재하지 않습니다.")); + + final String s3Key = repoAnalyze.getDocsKey(); + + // 1. S3에서 ZIP 파일 다운로드 + File zipFile = downloadZipFromS3(bucketName, s3Key); + + // 2. ZIP 파일 압축 해제 + File extractedDir = unzipFile(zipFile); + + // 3. 폴더가 비어있을 경우 예외 처리 + if (extractedDir.listFiles() == null || extractedDir.listFiles().length == 0) { + throw new EmptyFolderException("압축 해제된 폴더가 비어 있습니다."); + } + + // 3. .md 파일을 FileDetail 형식으로 변환하여 분류 + Map> categorizedFiles = collectAndCategorizeMarkdownFiles(extractedDir); + + // 4. 임시 파일 삭제 + zipFile.delete(); + deleteDirectory(extractedDir); + + return new DownloadAiAnalyzeResponse(categorizedFiles.get("summary"), categorizedFiles.get("regular")); + } + + private File downloadZipFromS3(String bucketName, String s3Key) throws IOException { + File tempZipFile = File.createTempFile("s3-download", ".zip"); + + System.out.println("==============================================="); + System.out.println("bucketName:" + bucketName); + System.out.println("s3key:" + s3Key); + System.out.println("==============================================="); + + try (InputStream inputStream = amazonS3Client.getObject(bucketName, "result/" + s3Key).getObjectContent(); + FileOutputStream outputStream = new FileOutputStream(tempZipFile)) { + + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } catch (Exception e) { + throw new NoExistRepoAnalyzeException("레포지토리 결과물을 아직 생성중입니다. 잠시만 기다려주세요."); + } + + return tempZipFile; + } + + private File unzipFile(File zipFile) throws IOException { + File outputDir = new File(zipFile.getParent(), "extracted"); + if (!outputDir.exists()) { + outputDir.mkdirs(); + } + + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + File newFile = new File(outputDir, entry.getName()); + + if (entry.isDirectory()) { + newFile.mkdirs(); + } else { + new File(newFile.getParent()).mkdirs(); + try (FileOutputStream fos = new FileOutputStream(newFile)) { + byte[] buffer = new byte[1024]; + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + } + } + zis.closeEntry(); + } + } + + return outputDir; + } + + private Map> collectAndCategorizeMarkdownFiles(File directory) throws IOException { + List summaryFiles = new ArrayList<>(); + List regularFiles = new ArrayList<>(); + File[] files = directory.listFiles(); + + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + Map> subCategory = collectAndCategorizeMarkdownFiles(file); + summaryFiles.addAll(subCategory.get("summary")); + regularFiles.addAll(subCategory.get("regular")); + } else if (file.getName().endsWith(".md")) { + DownloadAiAnalyzeResponse.FileDetail fileDetail = new DownloadAiAnalyzeResponse.FileDetail(file.getName(), readFileContent(file)); + + if (file.getName().contains("_summary")) { + summaryFiles.add(fileDetail); + } else { + regularFiles.add(fileDetail); + } + } + } + } + + Map> categorizedFiles = new HashMap<>(); + categorizedFiles.put("summary", summaryFiles); + categorizedFiles.put("regular", regularFiles); + + return categorizedFiles; + } + + private String readFileContent(File file) throws IOException { + StringBuilder content = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + content.append(line).append(System.lineSeparator()); + } + } + return content.toString(); + } + + private void deleteDirectory(File directory) { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + file.delete(); + } + } + } + directory.delete(); + } + + + + + + + + ///// // 리드미 업데이트 + public void updateFileContent(String repoName, String fileName, String newContent) throws IOException { + // 1. 레포지토리 정보 확인 + RepoAnalyze repoAnalyze = repoAnalyzeRepository.findByRepositoryName(repoName) + .orElseThrow(() -> new NoExistRepoAnalyzeException("레포지토리 정보가 존재하지 않습니다.")); + + String s3Key = repoAnalyze.getDocsKey(); + + // 2. S3에서 ZIP 파일 다운로드 + File zipFile = downloadZipFromS3(bucketName, s3Key); + + // 3. ZIP 파일 압축 해제 + File extractedDir = unzipFile(zipFile); + + // 4. 파일 내용 업데이트 + updateMarkdownFileContent(extractedDir, fileName, newContent); + + // 5. ZIP 파일 다시 생성 및 업로드 + File updatedZip = createZipFromDirectory(extractedDir); + uploadZipToS3(bucketName, s3Key, updatedZip); + + // 6. 임시 파일 삭제 + zipFile.delete(); + updatedZip.delete(); + deleteDirectory(extractedDir); + } + + private void updateMarkdownFileContent(File directory, String fileName, String newContent) throws IOException { + File[] files = directory.listFiles(); + + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + updateMarkdownFileContent(file, fileName, newContent); + } else if (file.getName().equals(fileName)) { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + writer.write(newContent); + } + return; + } + } + } + throw new FileNotFoundException("파일을 찾을 수 없습니다: " + fileName); + } + + private File createZipFromDirectory(File directory) throws IOException { + File zipFile = File.createTempFile("updated", ".zip"); + + try (FileOutputStream fos = new FileOutputStream(zipFile); + BufferedOutputStream bos = new BufferedOutputStream(fos); + java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(bos)) { + + zipDirectory(directory, directory.getPath(), zos); + } + + return zipFile; + } + + private void zipDirectory(File folder, String basePath, java.util.zip.ZipOutputStream zos) throws IOException { + File[] files = folder.listFiles(); + + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + zipDirectory(file, basePath, zos); + } else { + String zipEntryName = file.getPath().substring(basePath.length() + 1); + zos.putNextEntry(new java.util.zip.ZipEntry(zipEntryName)); + + try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) { + byte[] buffer = new byte[1024]; + int read; + while ((read = bis.read(buffer)) != -1) { + zos.write(buffer, 0, read); + } + } + zos.closeEntry(); + } + } + } + } + + private void uploadZipToS3(String bucketName, String s3Key, File zipFile) { + amazonS3Client.putObject(bucketName, s3Key, zipFile); + } + + public DownloadAiAnalyzeResponse downloadAndProcessZipReadmeInfoByRepoName(final long repoId) throws IOException { + final RepoAnalyze repoAnalyze = repoAnalyzeRepository.findById(repoId) + .orElseThrow(() -> new NoExistRepoAnalyzeException("레포지토리 정보가 존재하지 않습니다.")); + + System.out.println("========================123123123123 🔥"); + System.out.println(repoAnalyze.getBranchName()); + System.out.println(repoAnalyze.getRepoUrl()); + System.out.println(repoAnalyze.getReadMeKey()); + System.out.println(repoAnalyze.getRepositoryName()); + System.out.println("========================123123123123 🔥"); + + final String s3Key = repoAnalyze.getDocsKey(); + + // 1. S3에서 ZIP 파일 다운로드 + File zipFile = downloadZipFromS3(bucketName, s3Key); + + // 2. ZIP 파일 압축 해제 + File extractedDir = unzipFile(zipFile); + + // 3. .md 파일을 FileDetail 형식으로 변환하여 분류 + Map> categorizedFiles = collectAndCategorizeMarkdownFiles(extractedDir); + + // 4. 임시 파일 삭제 + zipFile.delete(); + deleteDirectory(extractedDir); + + return new DownloadAiAnalyzeResponse(categorizedFiles.get("summary"), categorizedFiles.get("regular")); + } + + + public DownloadReadmeAnalyzeResponse downloadAndProcessZipReadmeInfo(final long repoRegisterId) throws IOException { + // 1. 레포지토리 정보 확인 + RepoAnalyze repoAnalyze = repoAnalyzeRepository.findById(repoRegisterId) + .orElseThrow(() -> new NoExistRepoAnalyzeException("레포지토리 정보가 존재하지 않습니다.")); + + // 2. readMeKey 가져오기 + String readMeKey = repoAnalyze.getReadMeKey(); + if (readMeKey == null || readMeKey.isEmpty()) { + throw new NoExistRepoAnalyzeException("readMeKey가 존재하지 않습니다."); + } + + // 3. S3에서 파일 다운로드 + String readMeContent = downloadFileFromS3(bucketName, readMeKey); + + // 4. 결과 반환 + return new DownloadReadmeAnalyzeResponse(readMeContent); + } + + private String downloadFileFromS3(String bucketName, String s3Key) throws IOException { + try (InputStream inputStream = amazonS3Client.getObject(bucketName, "result/" + s3Key).getObjectContent(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + + StringBuilder content = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + content.append(line).append(System.lineSeparator()); + } + return content.toString(); + } catch (Exception e) { + throw new NoExistRepoAnalyzeException("레포지토리 결과물을 아직 생성중입니다. 잠시만 기다려주세요."); + } + } +} \ No newline at end of file diff --git a/src/main/java/dododocs/dododocs/analyze/application/RepoRegisterService.java b/src/main/java/dododocs/dododocs/analyze/application/RepoRegisterService.java new file mode 100644 index 0000000..4bfd72f --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/application/RepoRegisterService.java @@ -0,0 +1,96 @@ +package dododocs.dododocs.analyze.application; + +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import dododocs.dododocs.analyze.domain.repository.RepoAnalyzeRepository; +import dododocs.dododocs.analyze.dto.FindRepoRegisterResponses; +import dododocs.dododocs.analyze.dto.UpdateChatbotDocsRepoAnalyzeReadyStatusRequest; +import dododocs.dododocs.analyze.dto.UpdateReadmeDocsRepoAnalyzeReadyStatusRequest; +import dododocs.dododocs.auth.domain.repository.MemberRepository; +import dododocs.dododocs.auth.exception.NoExistMemberException; +import dododocs.dododocs.member.domain.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@RequiredArgsConstructor +@Service +public class RepoRegisterService { + private final RepoAnalyzeRepository repoAnalyzeRepository; + private final MemberRepository memberRepository; + + public FindRepoRegisterResponses findRegisteredRepos(final long memberId) { + final Member member = memberRepository.findById(memberId) + .orElseThrow(NoExistMemberException::new); + + final List repoAnalyzes = repoAnalyzeRepository.findByMember(member); + return new FindRepoRegisterResponses(repoAnalyzes); + } + + public void removeRegisteredRepos(final long registeredRepoId) { + repoAnalyzeRepository.deleteById(registeredRepoId); + } + + public void updateReadmeDocsRepoAnalyzeReadyStatus(final UpdateReadmeDocsRepoAnalyzeReadyStatusRequest updateRepoAnalyzeReadyStatusRequest) { + final RepoAnalyze repoAnalyze = findByRepoUrl(updateRepoAnalyzeReadyStatusRequest.getRepoUrl()); + + repoAnalyzeRepository.save( + new RepoAnalyze( + repoAnalyze.getId(), + repoAnalyze.getRepositoryName(), + repoAnalyze.getBranchName(), + repoAnalyze.getReadMeKey(), + repoAnalyze.getDocsKey(), + repoAnalyze.getRepoUrl(), + repoAnalyze.getMember(), + + updateRepoAnalyzeReadyStatusRequest.isDocsCompleted() ? updateRepoAnalyzeReadyStatusRequest.isDocsCompleted() : repoAnalyze.isDocsCompleted(), + updateRepoAnalyzeReadyStatusRequest.isReadmeCompleted() ? updateRepoAnalyzeReadyStatusRequest.isReadmeCompleted() : repoAnalyze.isReadmeCompleted(), + repoAnalyze.isChatbotCompleted() + ) + ); + } + + + public void updateChatbotRepoAnalyzeReadyStatus(final UpdateChatbotDocsRepoAnalyzeReadyStatusRequest updateRepoAnalyzeReadyStatusRequest) { + final RepoAnalyze repoAnalyze = findByRepoUrl(updateRepoAnalyzeReadyStatusRequest.getRepoUrl()); + + repoAnalyzeRepository.save( + new RepoAnalyze( + repoAnalyze.getId(), + repoAnalyze.getRepositoryName(), + repoAnalyze.getBranchName(), + repoAnalyze.getReadMeKey(), + repoAnalyze.getDocsKey(), + repoAnalyze.getRepoUrl(), + repoAnalyze.getMember(), + repoAnalyze.isDocsCompleted(), + repoAnalyze.isReadmeCompleted(), + updateRepoAnalyzeReadyStatusRequest.isChatbotCompleted() + ) + ); + } + + private RepoAnalyze findByRepoUrl(final String repoUrl) { + final String[] parts = repoUrl.split("/"); + if (parts.length < 5) { + throw new IllegalArgumentException("Invalid repoUrl format: " + repoUrl); + } + + + final String userName = parts[3]; + final Member member = memberRepository.findByOriginName(userName); + + final String repoName = parts[4]; + final String branchName = parts[5]; + + System.out.println("userName: " + userName); + System.out.println("realUserName: " + member.getOriginName()); + System.out.println("realUseId: " + member.getId()); + System.out.println("repoName: " + repoName); + System.out.println("branchName: " + branchName); + + return repoAnalyzeRepository.findByMemberNameAndRepositoryNameAndBranchName(userName, repoName, branchName) + .orElseThrow(() -> new IllegalArgumentException("No RepoAnalyze found for repoName: " + repoName + " and branchName: " + branchName)); + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/domain/MemberOrganization.java b/src/main/java/dododocs/dododocs/analyze/domain/MemberOrganization.java new file mode 100644 index 0000000..a48715b --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/domain/MemberOrganization.java @@ -0,0 +1,30 @@ +package dododocs.dododocs.analyze.domain; + +import dododocs.dododocs.member.domain.Member; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "member_organization") +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Entity +public class MemberOrganization { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "name") + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + public MemberOrganization(final Member member, final String name) { + this.member = member; + this.name = name; + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/domain/RepoAnalyze.java b/src/main/java/dododocs/dododocs/analyze/domain/RepoAnalyze.java new file mode 100644 index 0000000..efb37ab --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/domain/RepoAnalyze.java @@ -0,0 +1,93 @@ +package dododocs.dododocs.analyze.domain; + +import dododocs.dododocs.chatbot.domain.ChatLog; +import dododocs.dododocs.global.BaseEntity; +import dododocs.dododocs.member.domain.Member; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Table(name = "repo_analyze") +@Getter +@Entity +@Setter +public class RepoAnalyze extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "repository_name") + private String repositoryName; + + @Column(name = "branch_name") + private String branchName; + + @Column(name = "readme_key", nullable = false) + private String readMeKey; + + @Column(name = "docs_key", nullable = true) // docs key 는 null 이 허용된다. (Java 파일이 없는 경우 null 이 나올 수 있음) + private String docsKey; + + @Column(name = "repo_url", nullable = false) + private String repoUrl; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Column(name = "docs_completed") + private boolean docsCompleted = false; + + @Column(name = "readme_completed") + private boolean readmeCompleted = false; + + @Column(name = "chatbot_completed") + private boolean chatbotCompleted = false; + + @OneToMany(mappedBy = "repoAnalyze", cascade = CascadeType.ALL, orphanRemoval = true) + private List chatLogs = new ArrayList<>(); + + protected RepoAnalyze() { + } + + public RepoAnalyze(final String repositoryName, final String branchName, final String readMeKey, final String docsKey, final String repoUrl, final Member member) { + this.repositoryName = repositoryName; + this.branchName = branchName; + this.readMeKey = readMeKey; + this.docsKey = docsKey; + this.repoUrl = repoUrl; + this.member = member; + } + + public RepoAnalyze(final long id, final String repositoryName, final String branchName, final String readMeKey, final String docsKey, final String repoUrl, final Member member) { + this.id = id; + this.repositoryName = repositoryName; + this.branchName = branchName; + this.readMeKey = readMeKey; + this.docsKey = docsKey; + this.repoUrl = repoUrl; + this.member = member; + } + + public RepoAnalyze(final long id, final String repositoryName, final String branchName, final String readMeKey, final String docsKey, final String repoUrl, final Member member, + final boolean docsCompleted, final boolean readmeCompleted, final boolean chatbotCompleted) { + this.id = id; + this.repositoryName = repositoryName; + this.branchName = branchName; + this.readMeKey = readMeKey; + this.docsKey = docsKey; + this.repoUrl = repoUrl; + this.member = member; + this.docsCompleted = docsCompleted; + this.readmeCompleted = readmeCompleted; + this.chatbotCompleted = chatbotCompleted; + } + + public RepoAnalyze(final long id, final String repositoryName) { + this.id = id; + this.repositoryName = repositoryName; + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/domain/repository/MemberOrganizationRepository.java b/src/main/java/dododocs/dododocs/analyze/domain/repository/MemberOrganizationRepository.java new file mode 100644 index 0000000..f4bd1c3 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/domain/repository/MemberOrganizationRepository.java @@ -0,0 +1,13 @@ +package dododocs.dododocs.analyze.domain.repository; + +import dododocs.dododocs.analyze.domain.MemberOrganization; +import dododocs.dododocs.member.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface MemberOrganizationRepository extends JpaRepository { + @Query("SELECT mo.name FROM MemberOrganization mo WHERE mo.member = :member") + List findOrganizationNamesByMember(final Member member); +} diff --git a/src/main/java/dododocs/dododocs/analyze/domain/repository/RepoAnalyzeRepository.java b/src/main/java/dododocs/dododocs/analyze/domain/repository/RepoAnalyzeRepository.java new file mode 100644 index 0000000..0a80be7 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/domain/repository/RepoAnalyzeRepository.java @@ -0,0 +1,24 @@ +package dododocs.dododocs.analyze.domain.repository; + +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import dododocs.dododocs.member.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface RepoAnalyzeRepository extends JpaRepository { + Optional findByRepositoryName(final String repositoryName); + List findByMember(final Member member); + void deleteById(final Long id); + + @Query("SELECT r FROM RepoAnalyze r WHERE r.member.originName = :originName AND r.repositoryName = :repositoryName AND r.branchName = :branchName") + Optional findByMemberNameAndRepositoryNameAndBranchName( + @Param("originName") final String originName, + @Param("repositoryName") final String repositoryName, + @Param("branchName") final String branchName + ); +} + diff --git a/src/main/java/dododocs/dododocs/analyze/dto/DeleteRepoRegisterRequest.java b/src/main/java/dododocs/dododocs/analyze/dto/DeleteRepoRegisterRequest.java new file mode 100644 index 0000000..12fde30 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/DeleteRepoRegisterRequest.java @@ -0,0 +1,15 @@ +package dododocs.dododocs.analyze.dto; + +import lombok.Getter; + +@Getter +public class DeleteRepoRegisterRequest { + private long registeredRepoId; + + private DeleteRepoRegisterRequest() { + } + + public DeleteRepoRegisterRequest(final long registeredRepoId) { + this.registeredRepoId = registeredRepoId; + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/dto/DownloadAiAnalyzeRequest.java b/src/main/java/dododocs/dododocs/analyze/dto/DownloadAiAnalyzeRequest.java new file mode 100644 index 0000000..a34a1c6 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/DownloadAiAnalyzeRequest.java @@ -0,0 +1,15 @@ +package dododocs.dododocs.analyze.dto; + +import lombok.Getter; + +@Getter +public class DownloadAiAnalyzeRequest { + private String repositoryName; + + private DownloadAiAnalyzeRequest() { + } + + public DownloadAiAnalyzeRequest(final String repositoryName) { + this.repositoryName = repositoryName; + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/dto/DownloadAiAnalyzeResponse.java b/src/main/java/dododocs/dododocs/analyze/dto/DownloadAiAnalyzeResponse.java new file mode 100644 index 0000000..770ac18 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/DownloadAiAnalyzeResponse.java @@ -0,0 +1,25 @@ +package dododocs.dododocs.analyze.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; +import java.util.Map; + +@Getter +public class DownloadAiAnalyzeResponse { + private List summaryFiles; + private List regularFiles; + + @Getter + @AllArgsConstructor + public static class FileDetail { + private String fileName; + private String fileContents; + } + + public DownloadAiAnalyzeResponse(List summaryFiles, List regularFiles) { + this.summaryFiles = summaryFiles; + this.regularFiles = regularFiles; + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/dto/DownloadReadmeAnalyzeResponse.java b/src/main/java/dododocs/dododocs/analyze/dto/DownloadReadmeAnalyzeResponse.java new file mode 100644 index 0000000..f3dca99 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/DownloadReadmeAnalyzeResponse.java @@ -0,0 +1,15 @@ +package dododocs.dododocs.analyze.dto; + +import lombok.Getter; + +@Getter +public class DownloadReadmeAnalyzeResponse { + private String contents; + + private DownloadReadmeAnalyzeResponse() { + } + + public DownloadReadmeAnalyzeResponse(final String contents) { + this.contents = contents; + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/dto/EmptyFolderException.java b/src/main/java/dododocs/dododocs/analyze/dto/EmptyFolderException.java new file mode 100644 index 0000000..936e369 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/EmptyFolderException.java @@ -0,0 +1,7 @@ +package dododocs.dododocs.analyze.dto; + +public class EmptyFolderException extends RuntimeException { + public EmptyFolderException(String message) { + super(message); + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/dto/ExternalAiZipAnalyzeRequest.java b/src/main/java/dododocs/dododocs/analyze/dto/ExternalAiZipAnalyzeRequest.java new file mode 100644 index 0000000..9249968 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/ExternalAiZipAnalyzeRequest.java @@ -0,0 +1,42 @@ +package dododocs.dododocs.analyze.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +import java.util.List; + +// repo_url, include_test, docs_key, readme_key, korean + +// docs_key : msung99_moheng_main_DOCS.zip +// readme_key : msung99_moheng_main_README.md + +@Getter +public class ExternalAiZipAnalyzeRequest { + // s3 key 값, 레포 주소 필요 + @JsonProperty("s3_path") + private String s3Key; + + @JsonProperty("repo_url") + private String repositoryUrl; + + @JsonProperty("include_test") + private boolean includeTest; + + @JsonProperty("blocks") + private List blocks; + + @JsonProperty("korean") + private boolean korean; + + private ExternalAiZipAnalyzeRequest() { + } + + public ExternalAiZipAnalyzeRequest(final String s3Key, final String repositoryUrl, final List blocks, final boolean includeTest, final boolean korean) { + this.s3Key = s3Key; + this.blocks = blocks; + this.repositoryUrl = repositoryUrl; + + this.includeTest = includeTest; + this.korean = korean; + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/dto/ExternalAiZipAnalyzeResponse.java b/src/main/java/dododocs/dododocs/analyze/dto/ExternalAiZipAnalyzeResponse.java new file mode 100644 index 0000000..97c0d80 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/ExternalAiZipAnalyzeResponse.java @@ -0,0 +1,22 @@ +package dododocs.dododocs.analyze.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +public class ExternalAiZipAnalyzeResponse { + + @JsonProperty("readme_s3_key") + private String readMeS3Key; + + @JsonProperty("docs_s3_key") + private String docsS3Key; + + private ExternalAiZipAnalyzeResponse() { + } + + public ExternalAiZipAnalyzeResponse(final String readMeS3Key, final String docsS3Key) { + this.readMeS3Key = readMeS3Key; + this.docsS3Key = docsS3Key; + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/dto/FileContentResponse.java b/src/main/java/dododocs/dododocs/analyze/dto/FileContentResponse.java new file mode 100644 index 0000000..9fde44c --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/FileContentResponse.java @@ -0,0 +1,12 @@ +package dododocs.dododocs.analyze.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class FileContentResponse { + private String fileName; + private String fileContents; +} + diff --git a/src/main/java/dododocs/dododocs/analyze/dto/FindGitRepoContentRequest.java b/src/main/java/dododocs/dododocs/analyze/dto/FindGitRepoContentRequest.java new file mode 100644 index 0000000..cafcf05 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/FindGitRepoContentRequest.java @@ -0,0 +1,13 @@ +package dododocs.dododocs.analyze.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class FindGitRepoContentRequest { + private String repositoryName; + private String branchName; +} diff --git a/src/main/java/dododocs/dododocs/analyze/dto/FindRepoContentResponses.java b/src/main/java/dododocs/dododocs/analyze/dto/FindRepoContentResponses.java new file mode 100644 index 0000000..4eef27c --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/FindRepoContentResponses.java @@ -0,0 +1,15 @@ +package dododocs.dododocs.analyze.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class FindRepoContentResponses { + private List> contents; +} diff --git a/src/main/java/dododocs/dododocs/analyze/dto/FindRepoRegisterResponses.java b/src/main/java/dododocs/dododocs/analyze/dto/FindRepoRegisterResponses.java new file mode 100644 index 0000000..5d14cd0 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/FindRepoRegisterResponses.java @@ -0,0 +1,28 @@ +package dododocs.dododocs.analyze.dto; + +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +public class FindRepoRegisterResponses { + private List registeredRepoResponses; + + private FindRepoRegisterResponses() { + } + + public FindRepoRegisterResponses(final List repoAnalyzes) { + this.registeredRepoResponses = toResponses(repoAnalyzes); + } + + private List toResponses(final List repoAnalyzes) { + return repoAnalyzes.stream() + .map(RegisteredRepoResponse::new) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/dto/RegisteredRepoResponse.java b/src/main/java/dododocs/dododocs/analyze/dto/RegisteredRepoResponse.java new file mode 100644 index 0000000..3d73167 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/RegisteredRepoResponse.java @@ -0,0 +1,20 @@ +package dododocs.dododocs.analyze.dto; + +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import lombok.Getter; + +@Getter +public class RegisteredRepoResponse { + private long registeredRepoId; + private String repositoryName; + private String branchName; + + private RegisteredRepoResponse() { + } + + public RegisteredRepoResponse(final RepoAnalyze repoAnalyze) { + this.registeredRepoId = repoAnalyze.getId(); + this.repositoryName = repoAnalyze.getRepositoryName(); + this.branchName = repoAnalyze.getBranchName(); + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/dto/RepositoryContentDto.java b/src/main/java/dododocs/dododocs/analyze/dto/RepositoryContentDto.java new file mode 100644 index 0000000..dfe329a --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/RepositoryContentDto.java @@ -0,0 +1,19 @@ +package dododocs.dododocs.analyze.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class RepositoryContentDto { + private String name; + private String type; // "file" or "directory" + private String content; // 파일 내용 (type이 "file"일 때만 사용) + private List children; // 하위 폴더 및 파일 +} \ No newline at end of file diff --git a/src/main/java/dododocs/dododocs/analyze/dto/UpdateChatbotDocsRepoAnalyzeReadyStatusRequest.java b/src/main/java/dododocs/dododocs/analyze/dto/UpdateChatbotDocsRepoAnalyzeReadyStatusRequest.java new file mode 100644 index 0000000..608b3b6 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/UpdateChatbotDocsRepoAnalyzeReadyStatusRequest.java @@ -0,0 +1,13 @@ +package dododocs.dododocs.analyze.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class UpdateChatbotDocsRepoAnalyzeReadyStatusRequest { + private String repoUrl; + private boolean chatbotCompleted; +} diff --git a/src/main/java/dododocs/dododocs/analyze/dto/UpdateDocsRequest.java b/src/main/java/dododocs/dododocs/analyze/dto/UpdateDocsRequest.java new file mode 100644 index 0000000..2f17ea7 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/UpdateDocsRequest.java @@ -0,0 +1,15 @@ +package dododocs.dododocs.analyze.dto; + +import lombok.Getter; + +@Getter +public class UpdateDocsRequest { + private String fileName; + + private UpdateDocsRequest() { + } + + public UpdateDocsRequest(final String fileName) { + this.fileName = fileName; + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/dto/UpdateReadmeDocsRepoAnalyzeReadyStatusRequest.java b/src/main/java/dododocs/dododocs/analyze/dto/UpdateReadmeDocsRepoAnalyzeReadyStatusRequest.java new file mode 100644 index 0000000..f9fc1be --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/UpdateReadmeDocsRepoAnalyzeReadyStatusRequest.java @@ -0,0 +1,14 @@ +package dododocs.dododocs.analyze.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class UpdateReadmeDocsRepoAnalyzeReadyStatusRequest { + private String repoUrl; + private boolean readmeCompleted; + private boolean docsCompleted; +} diff --git a/src/main/java/dododocs/dododocs/analyze/dto/UploadGitRepoContentToS3Request.java b/src/main/java/dododocs/dododocs/analyze/dto/UploadGitRepoContentToS3Request.java new file mode 100644 index 0000000..52574b4 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/dto/UploadGitRepoContentToS3Request.java @@ -0,0 +1,46 @@ +package dododocs.dododocs.analyze.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class UploadGitRepoContentToS3Request { + private String repositoryName; + private String branchName; + private boolean korean; + private boolean includeTest; +} + +// private List readmeBlocks; + /* private boolean previewBlock; + private boolean overviewBlock; + private boolean analysisBlock; + private boolean structureBlock; + private boolean startBlock; + private boolean motivationBlock; + private boolean demoBlock; + private boolean deploymentBlock; + private boolean contributorsBlock; + private boolean faqBlock; + private boolean performanceBlock; */ + +/* +README_BLOCKS = { + "PREVIEW_BLOCK": PREVIEW_BLOCK, + "OVERVIEW_BLOCK": OVERVIEW_BLOCK, + "ANALYSIS_BLOCK": ANALYSIS_BLOCK, + "STRUCTURE_BLOCK": STRUCTURE_BLOCK, + "START_BLOCK": START_BLOCK, + "MOTIVATION_BLOCK": MOTIVATION_BLOCK, + "DEMO_BLOCK": DEMO_BLOCK, + "DEPLOYMENT_BLOCK": DEPLOYMENT_BLOCK, + "CONTRIBUTORS_BLOCK": CONTRIBUTORS_BLOCK, + "FAQ_BLOCK": FAQ_BLOCK, + "PERFORMANCE_BLOCK": PERFORMANCE_BLOCK, +} + */ \ No newline at end of file diff --git a/src/main/java/dododocs/dododocs/analyze/exception/MaxSizeRepoRegiserException.java b/src/main/java/dododocs/dododocs/analyze/exception/MaxSizeRepoRegiserException.java new file mode 100644 index 0000000..f7a5834 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/exception/MaxSizeRepoRegiserException.java @@ -0,0 +1,7 @@ +package dododocs.dododocs.analyze.exception; + +public class MaxSizeRepoRegiserException extends RuntimeException { + public MaxSizeRepoRegiserException(final String message) { + super(message); + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/exception/NoExistGitRepoException.java b/src/main/java/dododocs/dododocs/analyze/exception/NoExistGitRepoException.java new file mode 100644 index 0000000..9c4fa97 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/exception/NoExistGitRepoException.java @@ -0,0 +1,7 @@ +package dododocs.dododocs.analyze.exception; + +public class NoExistGitRepoException extends RuntimeException { + public NoExistGitRepoException(String message) { + super(message); + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/exception/NoExistRepoAnalyzeException.java b/src/main/java/dododocs/dododocs/analyze/exception/NoExistRepoAnalyzeException.java new file mode 100644 index 0000000..c2515db --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/exception/NoExistRepoAnalyzeException.java @@ -0,0 +1,7 @@ +package dododocs.dododocs.analyze.exception; + +public class NoExistRepoAnalyzeException extends RuntimeException { + public NoExistRepoAnalyzeException(final String message) { + super(message); + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/infrastructure/ExternalAiZipAnalyzeClient.java b/src/main/java/dododocs/dododocs/analyze/infrastructure/ExternalAiZipAnalyzeClient.java new file mode 100644 index 0000000..59b0da5 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/infrastructure/ExternalAiZipAnalyzeClient.java @@ -0,0 +1,49 @@ +package dododocs.dododocs.analyze.infrastructure; + +import dododocs.dododocs.analyze.dto.ExternalAiZipAnalyzeRequest; +import dododocs.dododocs.analyze.dto.ExternalAiZipAnalyzeResponse; +import dododocs.dododocs.auth.exception.NoExistMemberException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.beans.factory.annotation.Value; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class ExternalAiZipAnalyzeClient { + private final String aiBasicUrl; + private final String AI_ZIP_DOWNLOAD_AND_ANALYZE_REQUEST_URL_PREFIX = "/generate"; + private final RestTemplate restTemplate; + + public ExternalAiZipAnalyzeClient(final RestTemplate restTemplate, + @Value("${ai.basic_url}") final String aiBasicUrl) { + this.restTemplate = restTemplate; + this.aiBasicUrl = aiBasicUrl; + } + + public ExternalAiZipAnalyzeResponse requestAiZipDownloadAndAnalyze(final ExternalAiZipAnalyzeRequest request) { + return requestAnalyze(request); + } + + private ExternalAiZipAnalyzeResponse requestAnalyze(final ExternalAiZipAnalyzeRequest request) { + final Map uriVariables = new HashMap<>(); + + final ResponseEntity responseEntity = restTemplate.exchange( + aiBasicUrl + AI_ZIP_DOWNLOAD_AND_ANALYZE_REQUEST_URL_PREFIX, + HttpMethod.POST, + new HttpEntity<>(request), + ExternalAiZipAnalyzeResponse.class, + uriVariables + ); + + if(responseEntity.getStatusCode().is2xxSuccessful()) { + return responseEntity.getBody(); + } + throw new NoExistMemberException(); + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/presentation/AnalyzeController.java b/src/main/java/dododocs/dododocs/analyze/presentation/AnalyzeController.java index 30098ec..beef303 100644 --- a/src/main/java/dododocs/dododocs/analyze/presentation/AnalyzeController.java +++ b/src/main/java/dododocs/dododocs/analyze/presentation/AnalyzeController.java @@ -4,19 +4,21 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import dododocs.dododocs.analyze.application.AnalyzeService; +import dododocs.dododocs.analyze.dto.*; +import dododocs.dododocs.auth.dto.Accessor; +import dododocs.dododocs.auth.presentation.authentication.Authentication; +import org.hibernate.sql.Update; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.List; @@ -25,96 +27,42 @@ import java.util.zip.ZipOutputStream; @RestController -@RequestMapping("/api/download") +@RequestMapping("/api") public class AnalyzeController { private final AnalyzeService analyzeService; - private final RestTemplate restTemplate = new RestTemplate(); - private final String githubApiUrl = "https://api.github.com/repos/{owner}/{repo}/contents/{path}"; - private final ObjectMapper objectMapper = new ObjectMapper(); public AnalyzeController(final AnalyzeService analyzeService) { this.analyzeService = analyzeService; } - @GetMapping("/github-folder") - public ResponseEntity downloadGithubFolderAsZip() throws Exception { - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + // 1. 등록한 레포 리스트 뭐 되어있는지 조회 (유저당 최대 3개 레포 등록 가능) + // 2. 레포 등록시 DTO 수정 (korean, test 코드 넣을꺼임?) + // 3. 리드미 문자열 수정한거 DB 에 반영하기 + // => Test API 에 리드미 수정한거 반영하기 - try (ZipOutputStream zipOut = new ZipOutputStream(byteArrayOutputStream)) { - String owner = "msung99"; - String repo = "Gatsby-Starter-Haon"; - String path = ""; // 예: src/main/resources - addFolderToZip(owner, repo, path, zipOut, ""); - } - - ByteArrayResource resource = new ByteArrayResource(byteArrayOutputStream.toByteArray()); - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"github_folder.zip\""); - - return ResponseEntity.ok() - .headers(headers) - .contentLength(resource.contentLength()) - .body(resource); + @PostMapping("/upload/s3") + public void uploadGithubToS3(@Authentication final Accessor accessor, + @RequestBody final UploadGitRepoContentToS3Request uploadToS3Request) { + // s3 key 값, 레포 주소 필요 + analyzeService.uploadGithubRepoToS3(uploadToS3Request, accessor.getId()); } - private void addFolderToZip(String owner, String repo, String path, ZipOutputStream zipOut, String parentPath) throws Exception { - String url = githubApiUrl.replace("{owner}", owner) - .replace("{repo}", repo) - .replace("{path}", path); - - Object response = restTemplate.getForObject(url, Object.class); + /* @PutMapping("/docs/update/{registeredRepoId}") + public ResponseEntity updateDocsContents(@Authentication final Accessor accessor, + @PathVariable final Long registeredRepoId, + @RequestBody final UpdateDocsRequest updateDocsRequest) throws Exception { + analyzeService.updateDocsContents(updateDocsRequest.getFileName()); + return ResponseEntity.noContent().build(); + } */ - if (response instanceof List) { - List> fileList = objectMapper.convertValue(response, new TypeReference>>() {}); - for (Map fileData : fileList) { - String type = (String) fileData.get("type"); - String name = (String) fileData.get("name"); - String downloadUrl = (String) fileData.get("download_url"); - String filePath = parentPath + name; - - if ("file".equals(type) && downloadUrl != null) { - System.out.println("Adding file to ZIP: " + filePath + " from " + downloadUrl); - try (InputStream inputStream = new URL(downloadUrl).openStream()) { - zipOut.putNextEntry(new ZipEntry(filePath)); - inputStream.transferTo(zipOut); - zipOut.closeEntry(); - } catch (Exception e) { - System.err.println("파일을 추가하는 중 오류 발생: " + filePath); - e.printStackTrace(); - } - } else if ("dir".equals(type)) { - System.out.println("Entering directory: " + filePath); - addFolderToZip(owner, repo, path + "/" + name, zipOut, filePath + "/"); - } - } - } else { - System.out.println("응답이 파일 목록이 아닙니다."); - } + @GetMapping("/repo/contents") + public RepositoryContentDto getRepoContents(@Authentication final Accessor accessor, + @RequestBody final FindGitRepoContentRequest findGitRepoContentRequest) throws IOException { + return analyzeService.getRepositoryContents(accessor.getId(), findGitRepoContentRequest.getRepositoryName(), findGitRepoContentRequest.getBranchName()); } - @GetMapping("/github-repo-zip") - public ResponseEntity downloadGithubRepositoryAsZip() throws Exception { - String owner = "msung99"; // => gitHub 사용자명 또는 조직명 - String repo = "Gatsby-Starter-Haon"; // => 레포지토리 이름 - String branch = "main"; // => main - - String downloadUrl = String.format("https://github.com/%s/%s/archive/refs/heads/%s.zip", owner, repo, branch); - - // URL에서 InputStream 가져오기 - InputStream inputStream = new URL(downloadUrl).openStream(); - InputStreamResource resource = new InputStreamResource(inputStream); - - // HTTP 헤더 설정 - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + repo + "-" + branch + ".zip\""); - - analyzeService.uploadZipToS3(repo); - - return ResponseEntity.ok() - .headers(headers) - .contentType(MediaType.APPLICATION_OCTET_STREAM) - .body(resource); - } + // 업로딩 되고있는(= 아직 AI 로 부터 챗봇, docs, readme 중 단 하나라도 제공받지 못하는 레포가 있는 경우) 레포지토리 있으면 -> ID 값 리턴 + // else => null 리턴 } diff --git a/src/main/java/dododocs/dododocs/analyze/presentation/RepoRegisterController.java b/src/main/java/dododocs/dododocs/analyze/presentation/RepoRegisterController.java new file mode 100644 index 0000000..448e02d --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/presentation/RepoRegisterController.java @@ -0,0 +1,35 @@ +package dododocs.dododocs.analyze.presentation; + +import dododocs.dododocs.analyze.application.RepoRegisterService; +import dododocs.dododocs.analyze.dto.DeleteRepoRegisterRequest; +import dododocs.dododocs.analyze.dto.UpdateChatbotDocsRepoAnalyzeReadyStatusRequest; +import dododocs.dododocs.analyze.dto.UpdateReadmeDocsRepoAnalyzeReadyStatusRequest; +import dododocs.dododocs.auth.dto.Accessor; +import dododocs.dododocs.auth.presentation.authentication.Authentication; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/register") +@RequiredArgsConstructor +public class RepoRegisterController { + private final RepoRegisterService repoRegisterService; + + @DeleteMapping + public ResponseEntity deleteRegisteredRepos(@Authentication final Accessor accessor, + final @RequestBody DeleteRepoRegisterRequest deleteRepoRegisterRequest) { + repoRegisterService.removeRegisteredRepos(deleteRepoRegisterRequest.getRegisteredRepoId()); + return ResponseEntity.noContent().build(); + } + + @PutMapping("/status/readme/docs") + public void updateRepoAnalyzeReadmeDocsReadyStatus(final @RequestBody UpdateReadmeDocsRepoAnalyzeReadyStatusRequest updateRepoAnalyzeReadyStatusRequest) { + repoRegisterService.updateReadmeDocsRepoAnalyzeReadyStatus(updateRepoAnalyzeReadyStatusRequest); + } + + @PutMapping("/status/chatbot") + public void updateRepoAnalyzeChatbotReadyStatus(final @RequestBody UpdateChatbotDocsRepoAnalyzeReadyStatusRequest updateChatbotDocsRepoAnalyzeReadyStatusRequest) { + repoRegisterService.updateChatbotRepoAnalyzeReadyStatus(updateChatbotDocsRepoAnalyzeReadyStatusRequest); + } +} diff --git a/src/main/java/dododocs/dododocs/analyze/presentation/S3DownloadController.java b/src/main/java/dododocs/dododocs/analyze/presentation/S3DownloadController.java new file mode 100644 index 0000000..082a020 --- /dev/null +++ b/src/main/java/dododocs/dododocs/analyze/presentation/S3DownloadController.java @@ -0,0 +1,64 @@ +package dododocs.dododocs.analyze.presentation; + + +import dododocs.dododocs.analyze.application.DownloadFromS3Service; +import dododocs.dododocs.analyze.dto.DownloadAiAnalyzeRequest; +import dododocs.dododocs.analyze.dto.DownloadAiAnalyzeResponse; +import dododocs.dododocs.analyze.dto.DownloadReadmeAnalyzeResponse; +import dododocs.dododocs.analyze.dto.FileContentResponse; +import dododocs.dododocs.auth.dto.Accessor; +import dododocs.dododocs.auth.presentation.authentication.Authentication; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.apache.catalina.LifecycleState; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.Map; + +@RequestMapping("/api") +@RestController +@RequiredArgsConstructor +public class S3DownloadController { + private final DownloadFromS3Service s3DownloadService; + private final DownloadFromS3Service downloadFromS3Service; + + @PostMapping("/download/docs/{registeredRepoId}") + public ResponseEntity downloadAIDocumentAnalyzeResultFromS3(@Authentication final Accessor accessor, + @PathVariable final Long registeredRepoId) throws Exception { + return ResponseEntity.ok(s3DownloadService.downloadAndProcessZipDocsInfo(registeredRepoId)); + } + + @PostMapping("/download/readme/{registeredRepoId}") + public ResponseEntity downloadReadmeFromS3(@Authentication final Accessor accessor, + @PathVariable final Long registeredRepoId) throws Exception { + return ResponseEntity.ok(s3DownloadService.downloadAndProcessZipReadmeInfo(registeredRepoId)); + } + + @GetMapping("/download/s3/detail/{registeredRepoId}") + public FileContentResponse getFileContentByFileName(@PathVariable final Long registeredRepoId, + @RequestParam final String fileName) throws Exception { + DownloadAiAnalyzeResponse response = s3DownloadService.downloadAndProcessZipReadmeInfoByRepoName(registeredRepoId); + + // 검색 로직 + return response.getSummaryFiles().stream() + .filter(file -> fileName.equals(file.getFileName())) + .findFirst() + .or(() -> response.getRegularFiles().stream() + .filter(file -> fileName.equals(file.getFileName())) + .findFirst()) + .map(file -> new FileContentResponse(file.getFileName(), file.getFileContents())) + .orElseThrow(() -> new RuntimeException("File not found")); + } + + @PutMapping("/readme") + public ResponseEntity updateFileContent( + @Authentication final Accessor accessor, + @RequestParam String repositoryName, @RequestParam String fileName, @RequestParam String newContent) throws Exception { + downloadFromS3Service.updateFileContent(repositoryName, fileName, newContent); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/src/main/java/dododocs/dododocs/auth/application/AuthService.java b/src/main/java/dododocs/dododocs/auth/application/AuthService.java index ee9a63f..529d18d 100644 --- a/src/main/java/dododocs/dododocs/auth/application/AuthService.java +++ b/src/main/java/dododocs/dododocs/auth/application/AuthService.java @@ -1,15 +1,19 @@ package dododocs.dododocs.auth.application; +import dododocs.dododocs.auth.dto.GithubOAuthMemberWithAccessToken; import dododocs.dododocs.auth.exception.NoExistMemberException; import dododocs.dododocs.auth.infrastructure.GithubOAuthClient; import dododocs.dododocs.auth.infrastructure.GithubOAuthMember; import dododocs.dododocs.auth.domain.GithubOAuthUriProvider; import dododocs.dododocs.auth.domain.JwtTokenCreator; import dododocs.dododocs.auth.domain.repository.MemberRepository; +import dododocs.dododocs.auth.infrastructure.GithubOrganizationClient; import dododocs.dododocs.member.domain.Member; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@RequiredArgsConstructor @Transactional(readOnly = true) @Service public class AuthService { @@ -17,40 +21,37 @@ public class AuthService { private final JwtTokenCreator jwtTokenCreator; private final GithubOAuthClient githubOAuthClient; private final MemberRepository memberRepository; - - public AuthService(final GithubOAuthUriProvider githubOAuthUriProvider, - final JwtTokenCreator jwtTokenCreator, - final GithubOAuthClient githubOAuthClient, - final MemberRepository memberRepository) { - this.githubOAuthUriProvider = githubOAuthUriProvider; - this.jwtTokenCreator = jwtTokenCreator; - this.githubOAuthClient = githubOAuthClient; - this.memberRepository = memberRepository; - } + private final GithubOrganizationClient githubOrganizationClient; public String generateUri() { return githubOAuthUriProvider.generateUri(); } - public String generateTokenWithCode(final String code) { - final GithubOAuthMember githubOAuthMember = githubOAuthClient.getOAuthMember(code); - final Member foundMember = findOrCreateMember(githubOAuthMember); + @Transactional + public String generateTokenWithCode(final String code) throws Exception { + final GithubOAuthMemberWithAccessToken githubOAuthMemberWithAccessToken = githubOAuthClient.getOAuthMember(code); + final GithubOAuthMember githubOAuthMember = githubOAuthMemberWithAccessToken.getGithubOAuthMember(); + final String githubAccessToken = githubOAuthMemberWithAccessToken.getAccessToken(); + + final Member foundMember = findOrCreateMember(githubOAuthMemberWithAccessToken.getGithubOAuthMember(), githubAccessToken); + + githubOrganizationClient.saveMemberOrganizationNames(foundMember, githubOAuthMember.getOriginName()); final String accessToken = jwtTokenCreator.createToken(foundMember.getId()); return accessToken; } - public Member findOrCreateMember(final GithubOAuthMember githubOAuthmember) { - final String email = githubOAuthmember.getEmail(); + public Member findOrCreateMember(final GithubOAuthMember githubOAuthmember, final String githubAccessToken) { + final Long socialLoginId = githubOAuthmember.getSocialLoginId(); - if(!memberRepository.existsByEmail(email)) { - memberRepository.save(generateMember(githubOAuthmember)); + if(!memberRepository.existsBySocialLoginId(socialLoginId)) { + memberRepository.save(generateMember(githubOAuthmember, githubAccessToken)); } - final Member foundMember = memberRepository.findByEmail(email); + final Member foundMember = memberRepository.findBySocialLoginId(socialLoginId); return foundMember; } - private Member generateMember(final GithubOAuthMember githubOAuthMember) { - return new Member(githubOAuthMember.getEmail(), githubOAuthMember.getNickName(), githubOAuthMember.getOriginName()); + private Member generateMember(final GithubOAuthMember githubOAuthMember, final String githubAccessToken) { + return new Member(githubOAuthMember.getSocialLoginId(), githubOAuthMember.getNickName(), githubOAuthMember.getOriginName(), githubAccessToken); } public Long extractMemberId(final String accessToken) { diff --git a/src/main/java/dododocs/dododocs/auth/domain/repository/MemberRepository.java b/src/main/java/dododocs/dododocs/auth/domain/repository/MemberRepository.java index bbe8d3a..edb62d7 100644 --- a/src/main/java/dododocs/dododocs/auth/domain/repository/MemberRepository.java +++ b/src/main/java/dododocs/dododocs/auth/domain/repository/MemberRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { - boolean existsByEmail(String email); - Member findByEmail(String email); + boolean existsBySocialLoginId(final Long socialLoginId); + Member findBySocialLoginId(final Long socialLoginId); + Member findByOriginName(final String originName); } diff --git a/src/main/java/dododocs/dododocs/auth/dto/GithubOAuthMemberWithAccessToken.java b/src/main/java/dododocs/dododocs/auth/dto/GithubOAuthMemberWithAccessToken.java new file mode 100644 index 0000000..8042729 --- /dev/null +++ b/src/main/java/dododocs/dododocs/auth/dto/GithubOAuthMemberWithAccessToken.java @@ -0,0 +1,15 @@ +package dododocs.dododocs.auth.dto; + +import dododocs.dododocs.auth.infrastructure.GithubOAuthMember; +import lombok.Getter; + +@Getter +public class GithubOAuthMemberWithAccessToken { + private GithubOAuthMember githubOAuthMember; + private String accessToken; + + public GithubOAuthMemberWithAccessToken(final GithubOAuthMember githubOAuthMember, final String accessToken) { + this.githubOAuthMember = githubOAuthMember; + this.accessToken = accessToken; + } +} diff --git a/src/main/java/dododocs/dododocs/auth/infrastructure/GithubOAuthClient.java b/src/main/java/dododocs/dododocs/auth/infrastructure/GithubOAuthClient.java index e64db2b..2be2616 100644 --- a/src/main/java/dododocs/dododocs/auth/infrastructure/GithubOAuthClient.java +++ b/src/main/java/dododocs/dododocs/auth/infrastructure/GithubOAuthClient.java @@ -1,5 +1,6 @@ package dododocs.dododocs.auth.infrastructure; +import dododocs.dododocs.auth.dto.GithubOAuthMemberWithAccessToken; import dododocs.dododocs.auth.exception.InvalidOAuthException; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; @@ -35,8 +36,11 @@ public GithubOAuthClient(final RestTemplate restTemplate, this.clientSecret = clientSecret; } - public GithubOAuthMember getOAuthMember(final String code) { + public GithubOAuthMemberWithAccessToken getOAuthMember(final String code) { final String accessToken = requestGithubAccessToken(code); + + System.out.println("🚀 accessToken: " + accessToken); + final HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("Authorization", "token " + accessToken); final HttpEntity> userInfoRequestEntity = new HttpEntity<>(httpHeaders); @@ -49,7 +53,8 @@ public GithubOAuthMember getOAuthMember(final String code) { ); if(githubOAuthMember.getStatusCode().is2xxSuccessful()) { - return githubOAuthMember.getBody(); + GithubOAuthMember githubOAuthMemberBody = githubOAuthMember.getBody(); + return new GithubOAuthMemberWithAccessToken(githubOAuthMemberBody, accessToken); } throw new InvalidOAuthException("깃허브 소셜 로그인 제공처 서버에 예기치 못한 문제가 발생했습니다."); @@ -61,7 +66,7 @@ private String requestGithubAccessToken(final String code) { params.add("client_id", clientId); params.add("client_secret", clientSecret); - params.add("code",code); + params.add("code", code); params.add("redirect_uri", redirectUri); final HttpEntity> accessTokenRequestEntity = new HttpEntity<>(params, httpHeaders); diff --git a/src/main/java/dododocs/dododocs/auth/infrastructure/GithubOAuthMember.java b/src/main/java/dododocs/dododocs/auth/infrastructure/GithubOAuthMember.java index 5dc6f00..4822107 100644 --- a/src/main/java/dododocs/dododocs/auth/infrastructure/GithubOAuthMember.java +++ b/src/main/java/dododocs/dododocs/auth/infrastructure/GithubOAuthMember.java @@ -5,8 +5,8 @@ @Getter public class GithubOAuthMember { - @JsonProperty("email") - private String email; + @JsonProperty("id") + private Long socialLoginId; @JsonProperty("name") private String nickName; @@ -14,8 +14,8 @@ public class GithubOAuthMember { @JsonProperty("login") private String originName; - public GithubOAuthMember(final String email, final String nickName, final String originName) { - this.email = email; + public GithubOAuthMember(final Long socialLoginId, final String nickName, final String originName) { + this.socialLoginId = socialLoginId; this.nickName = nickName; this.originName = originName; } diff --git a/src/main/java/dododocs/dododocs/auth/infrastructure/GithubOrganizationClient.java b/src/main/java/dododocs/dododocs/auth/infrastructure/GithubOrganizationClient.java new file mode 100644 index 0000000..a479e72 --- /dev/null +++ b/src/main/java/dododocs/dododocs/auth/infrastructure/GithubOrganizationClient.java @@ -0,0 +1,35 @@ +package dododocs.dododocs.auth.infrastructure; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import dododocs.dododocs.analyze.domain.MemberOrganization; +import dododocs.dododocs.analyze.domain.repository.MemberOrganizationRepository; +import dododocs.dododocs.member.domain.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +@Component +public class GithubOrganizationClient { + private final MemberOrganizationRepository memberOrganizationRepository; + private final RestTemplate restTemplate; + + public void saveMemberOrganizationNames(final Member member, final String username) throws Exception { + String url = String.format("https://api.github.com/users/%s/orgs", username); + + ResponseEntity response = restTemplate.getForEntity(url, String.class); + + ObjectMapper objectMapper = new ObjectMapper(); + List> orgs = objectMapper.readValue(response.getBody(), new TypeReference>>() {}); + + for (Map org : orgs) { + String name = (String) org.get("login"); + memberOrganizationRepository.save(new MemberOrganization(member, name)); + } + } +} diff --git a/src/main/java/dododocs/dododocs/auth/presentation/AuthController.java b/src/main/java/dododocs/dododocs/auth/presentation/AuthController.java index b9eae02..57bbfe5 100644 --- a/src/main/java/dododocs/dododocs/auth/presentation/AuthController.java +++ b/src/main/java/dododocs/dododocs/auth/presentation/AuthController.java @@ -24,7 +24,7 @@ public ResponseEntity generateUri() { } @PostMapping("/login") - public ResponseEntity login(@RequestBody final LoginRequest loginRequest) { + public ResponseEntity login(@RequestBody final LoginRequest loginRequest) throws Exception { final String accessToken = authService.generateTokenWithCode(loginRequest.getCode()); return ResponseEntity.status(CREATED).body(new AccessTokenResponse(accessToken)); } diff --git a/src/main/java/dododocs/dododocs/chatbot/application/ChatbotService.java b/src/main/java/dododocs/dododocs/chatbot/application/ChatbotService.java new file mode 100644 index 0000000..d582ef5 --- /dev/null +++ b/src/main/java/dododocs/dododocs/chatbot/application/ChatbotService.java @@ -0,0 +1,60 @@ +package dododocs.dododocs.chatbot.application; + +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import dododocs.dododocs.analyze.domain.repository.RepoAnalyzeRepository; +import dododocs.dododocs.analyze.exception.NoExistRepoAnalyzeException; +import dododocs.dododocs.chatbot.domain.ChatLog; +import dododocs.dododocs.chatbot.domain.repository.ChatLogRepository; +import dododocs.dododocs.chatbot.dto.ExternalQuestToChatbotRequest; +import dododocs.dododocs.chatbot.dto.ExternalQuestToChatbotResponse; +import dododocs.dododocs.chatbot.dto.FindChatLogResponses; +import dododocs.dododocs.chatbot.infrastructure.ExternalChatbotClient; +import dododocs.dododocs.chatbot.infrastructure.ExternalChatbotClientByWebFlux; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +@RequiredArgsConstructor +@Service +public class ChatbotService { + private final ExternalChatbotClient externalChatbotClient; + private final ExternalChatbotClientByWebFlux externalChatbotClientByWebFlux; + private final RepoAnalyzeRepository repoAnalyzeRepository; + private final ChatLogRepository chatLogRepository; + + public ExternalQuestToChatbotResponse questionToChatbotAndSaveLogs(final long registeredRepoId, final String question) { + final RepoAnalyze repoAnalyze = repoAnalyzeRepository.findById(registeredRepoId) + .orElseThrow(() -> new NoExistRepoAnalyzeException("레포지토리 정보가 존재하지 않습니다.")); + + final List chatLogs = chatLogRepository.findTop3ByRepoAnalyzeOrderBySequenceDesc(repoAnalyze); + + final List recentChatLogs = chatLogs.stream() + .map(chatLog -> new ExternalQuestToChatbotRequest.RecentChatLog(chatLog.getQuestion(), chatLog.getAnswer())) + .toList(); + + final ExternalQuestToChatbotRequest questToChatbotRequest = new ExternalQuestToChatbotRequest( + repoAnalyze.getRepoUrl(), + question, + recentChatLogs, + false + ); + + System.out.println("url:" + repoAnalyze.getRepoUrl() + "/" + repoAnalyze.getBranchName()); + + final ExternalQuestToChatbotResponse externalQuestToChatbotResponse = externalChatbotClient.questToChatbot(questToChatbotRequest); + chatLogRepository.save(new ChatLog(question, externalQuestToChatbotResponse.getAnswer(), repoAnalyze)); + return externalQuestToChatbotResponse; + } + + public FindChatLogResponses findChatbotHistory(final long registeredRepoId, final Pageable pageable) { + final RepoAnalyze repoAnalyze = repoAnalyzeRepository.findById(registeredRepoId) + .orElseThrow(() -> new NoExistRepoAnalyzeException("레포 정보 없습니다.")); + + return new FindChatLogResponses(chatLogRepository.findByRepoAnalyzeOrderBySequenceWithPagination(repoAnalyze, pageable).getContent()); + } +} + diff --git a/src/main/java/dododocs/dododocs/chatbot/domain/ChatLog.java b/src/main/java/dododocs/dododocs/chatbot/domain/ChatLog.java new file mode 100644 index 0000000..39c7552 --- /dev/null +++ b/src/main/java/dododocs/dododocs/chatbot/domain/ChatLog.java @@ -0,0 +1,38 @@ +package dododocs.dododocs.chatbot.domain; + +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import jakarta.persistence.*; +import lombok.Getter; + +@Getter +@Table(name = "chat_log") +@Entity +public class ChatLog { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "question") + private String question; + + @Column(name = "answer", length = 8000) + private String answer; + + @Column(name = "sequence") + private Long sequence; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "repo_analyze_id", nullable = false) + private RepoAnalyze repoAnalyze; + + protected ChatLog() { + } + + public ChatLog(final String question, final String answer, final RepoAnalyze repoAnalyze) { + this.question = question; + this.answer = answer; + this.sequence = 0L; + this.repoAnalyze = repoAnalyze; + } +} diff --git a/src/main/java/dododocs/dododocs/chatbot/domain/repository/ChatLogRepository.java b/src/main/java/dododocs/dododocs/chatbot/domain/repository/ChatLogRepository.java new file mode 100644 index 0000000..a2b71f2 --- /dev/null +++ b/src/main/java/dododocs/dododocs/chatbot/domain/repository/ChatLogRepository.java @@ -0,0 +1,17 @@ +package dododocs.dododocs.chatbot.domain.repository; + +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import dododocs.dododocs.chatbot.domain.ChatLog; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.data.domain.Pageable; +import java.util.List; + +public interface ChatLogRepository extends JpaRepository { + List findTop3ByRepoAnalyzeOrderBySequenceDesc(final RepoAnalyze repoAnalyze); + + @Query("SELECT c FROM ChatLog c WHERE c.repoAnalyze = :repoAnalyze ORDER BY c.sequence DESC ") + Page findByRepoAnalyzeOrderBySequenceWithPagination(@Param("repoAnalyze") final RepoAnalyze repoAnalyze, final Pageable pageable); +} diff --git a/src/main/java/dododocs/dododocs/chatbot/dto/ExternalQuestToChatbotRequest.java b/src/main/java/dododocs/dododocs/chatbot/dto/ExternalQuestToChatbotRequest.java new file mode 100644 index 0000000..d2d81c8 --- /dev/null +++ b/src/main/java/dododocs/dododocs/chatbot/dto/ExternalQuestToChatbotRequest.java @@ -0,0 +1,47 @@ +package dododocs.dododocs.chatbot.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import dododocs.dododocs.chatbot.domain.ChatLog; +import lombok.Getter; + +import java.util.List; + +@Getter +public class ExternalQuestToChatbotRequest { + + @JsonProperty("repo_url") + private String repoUrl; + + @JsonProperty("query") + private String query; + + @JsonProperty("chat_history") + private List chatHistory; + + private boolean stream; + + @Getter + public static class RecentChatLog { + private String question; + private String answer; + + private RecentChatLog() { + } + + public RecentChatLog(final String question, final String answer) { + this.question = question; + this.answer = answer; + } + } + + private ExternalQuestToChatbotRequest() { + } + + public ExternalQuestToChatbotRequest(final String repoUrl, final String query, final List chatHistory, + final boolean stream) { + this.repoUrl = repoUrl; + this.query = query; + this.chatHistory = chatHistory; + this.stream = stream; + } +} diff --git a/src/main/java/dododocs/dododocs/chatbot/dto/ExternalQuestToChatbotResponse.java b/src/main/java/dododocs/dododocs/chatbot/dto/ExternalQuestToChatbotResponse.java new file mode 100644 index 0000000..7246477 --- /dev/null +++ b/src/main/java/dododocs/dododocs/chatbot/dto/ExternalQuestToChatbotResponse.java @@ -0,0 +1,15 @@ +package dododocs.dododocs.chatbot.dto; + +import lombok.Getter; + +@Getter +public class ExternalQuestToChatbotResponse { + private String answer; + + private ExternalQuestToChatbotResponse() { + } + + public ExternalQuestToChatbotResponse(final String answer) { + this.answer = answer; + } +} diff --git a/src/main/java/dododocs/dododocs/chatbot/dto/FindChatLogResponse.java b/src/main/java/dododocs/dododocs/chatbot/dto/FindChatLogResponse.java new file mode 100644 index 0000000..0db823a --- /dev/null +++ b/src/main/java/dododocs/dododocs/chatbot/dto/FindChatLogResponse.java @@ -0,0 +1,18 @@ +package dododocs.dododocs.chatbot.dto; + +import dododocs.dododocs.chatbot.domain.ChatLog; +import lombok.Getter; + +@Getter +public class FindChatLogResponse { + private String question; + private String answer; + + private FindChatLogResponse() { + } + + public FindChatLogResponse(final ChatLog chatLog) { + this.question = chatLog.getQuestion(); + this.answer = chatLog.getAnswer(); + } +} diff --git a/src/main/java/dododocs/dododocs/chatbot/dto/FindChatLogResponses.java b/src/main/java/dododocs/dododocs/chatbot/dto/FindChatLogResponses.java new file mode 100644 index 0000000..393f16d --- /dev/null +++ b/src/main/java/dododocs/dododocs/chatbot/dto/FindChatLogResponses.java @@ -0,0 +1,25 @@ +package dododocs.dododocs.chatbot.dto; + +import dododocs.dododocs.chatbot.domain.ChatLog; +import lombok.Getter; + +import java.util.List; +import java.util.stream.Collectors; + +@Getter +public class FindChatLogResponses { + private List findChatLogResponses; + + private FindChatLogResponses() { + } + + public FindChatLogResponses(final List chatLogs) { + findChatLogResponses = toResponses(chatLogs); + } + + private List toResponses(final List chatLogs) { + return chatLogs.stream() + .map(FindChatLogResponse::new) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/dododocs/dododocs/chatbot/dto/FindMemberInfoResponse.java b/src/main/java/dododocs/dododocs/chatbot/dto/FindMemberInfoResponse.java new file mode 100644 index 0000000..73501fe --- /dev/null +++ b/src/main/java/dododocs/dododocs/chatbot/dto/FindMemberInfoResponse.java @@ -0,0 +1,15 @@ +package dododocs.dododocs.chatbot.dto; + +import lombok.Getter; + +@Getter +public class FindMemberInfoResponse { + private String nickname; + + private FindMemberInfoResponse() { + } + + public FindMemberInfoResponse(final String nickname) { + this.nickname = nickname; + } +} diff --git a/src/main/java/dododocs/dododocs/chatbot/dto/QuestToChatbotRequest.java b/src/main/java/dododocs/dododocs/chatbot/dto/QuestToChatbotRequest.java new file mode 100644 index 0000000..af1e5eb --- /dev/null +++ b/src/main/java/dododocs/dododocs/chatbot/dto/QuestToChatbotRequest.java @@ -0,0 +1,15 @@ +package dododocs.dododocs.chatbot.dto; + +import lombok.Getter; + +@Getter +public class QuestToChatbotRequest { + private String question; + + private QuestToChatbotRequest() { + } + + public QuestToChatbotRequest(final String question) { + this.question = question; + } +} diff --git a/src/main/java/dododocs/dododocs/chatbot/dto/TestWebFluxResponse.java b/src/main/java/dododocs/dododocs/chatbot/dto/TestWebFluxResponse.java new file mode 100644 index 0000000..02d0ec0 --- /dev/null +++ b/src/main/java/dododocs/dododocs/chatbot/dto/TestWebFluxResponse.java @@ -0,0 +1,12 @@ +package dododocs.dododocs.chatbot.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class TestWebFluxResponse { + private String content; +} diff --git a/src/main/java/dododocs/dododocs/chatbot/infrastructure/ExternalChatbotClient.java b/src/main/java/dododocs/dododocs/chatbot/infrastructure/ExternalChatbotClient.java new file mode 100644 index 0000000..a0fc341 --- /dev/null +++ b/src/main/java/dododocs/dododocs/chatbot/infrastructure/ExternalChatbotClient.java @@ -0,0 +1,99 @@ +package dododocs.dododocs.chatbot.infrastructure; + +import dododocs.dododocs.analyze.exception.NoExistGitRepoException; +import dododocs.dododocs.analyze.exception.NoExistRepoAnalyzeException; +import dododocs.dododocs.chatbot.dto.ExternalQuestToChatbotRequest; +import dododocs.dododocs.chatbot.dto.ExternalQuestToChatbotResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class ExternalChatbotClient { + private final String aiBasicUrl; + private static final String CHATBOT_QUESTION_REQUEST_URL_PREFIX = "/chat"; + private final RestTemplate restTemplate; + + public ExternalChatbotClient(final RestTemplate restTemplate, + @Value("${ai.basic_url}") final String aiBasicUrl) { + this.restTemplate = restTemplate; + this.aiBasicUrl = aiBasicUrl; + } + + public ExternalQuestToChatbotResponse questToChatbot(final ExternalQuestToChatbotRequest questToChatbotRequest) { + return requestQuestion(questToChatbotRequest); + } + + private ExternalQuestToChatbotResponse requestQuestion(final ExternalQuestToChatbotRequest questToChatbotRequest) { + try { + final Map urlVariables = new HashMap<>(); + + final ResponseEntity responseEntity = restTemplate.exchange( + aiBasicUrl + CHATBOT_QUESTION_REQUEST_URL_PREFIX, + HttpMethod.POST, + new HttpEntity<>(questToChatbotRequest), + ExternalQuestToChatbotResponse.class, + urlVariables + ); + + return responseEntity.getBody(); + } catch (final HttpServerErrorException e) { + // 상태 코드 500 (서버 오류) 처리 + throw new NoExistRepoAnalyzeException("레포지토리 결과물을 아직 생성중입니다. 잠시만 기다려주세요."); + } catch (final RestClientException e) { + // 그 외 RestTemplate 관련 예외 처리 + throw new NoExistGitRepoException("레포지토리 결과물을 아직 생성중입니다. 잠시만 기다려주세요."); + } + } +} + + +/* +@Component +public class ExternalAiRecommendModelClient implements ExternalRecommendModelClient { + private static final String RECOMMEND_TRIP_LIST_REQUEST_URL = "http://ai:8000/travel/custom/model?page={page}"; + private final RestTemplate restTemplate; + + public ExternalAiRecommendModelClient(final RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + @Override + public RecommendTripsByVisitedLogsResponse recommendTripsByVisitedLogs(final RecommendTripsByVisitedLogsRequest request) { + return requestRecommendTrips(request); + } + + private RecommendTripsByVisitedLogsResponse requestRecommendTrips(final RecommendTripsByVisitedLogsRequest request) { + final Map uriVariables = new HashMap<>(); + uriVariables.put("page", String.valueOf(request.getPage())); + + final ResponseEntity responseEntity = fetchRecommendTripsByVisitedLogs(request, uriVariables); + return responseEntity.getBody(); + } + + private ResponseEntity fetchRecommendTripsByVisitedLogs(final RecommendTripsByVisitedLogsRequest request, final Map uriVariables) { + try { + return restTemplate.exchange( + RECOMMEND_TRIP_LIST_REQUEST_URL, + HttpMethod.POST, + new HttpEntity<>(new PreferredLocationRequest(request.getPreferredLocation())), + RecommendTripsByVisitedLogsResponse.class, + uriVariables + ); + } catch (final ResourceAccessException | HttpClientErrorException e) { + throw new InvalidAIServerException("AI 서버에 접근할 수 없는 상태입니다."); + } + catch (final RestClientException e) { + throw new InvalidAIServerException("AI 서버에 예기치 못한 오류가 발생했습니다."); + } + } +} + */ \ No newline at end of file diff --git a/src/main/java/dododocs/dododocs/chatbot/infrastructure/ExternalChatbotClientByWebFlux.java b/src/main/java/dododocs/dododocs/chatbot/infrastructure/ExternalChatbotClientByWebFlux.java new file mode 100644 index 0000000..af8acad --- /dev/null +++ b/src/main/java/dododocs/dododocs/chatbot/infrastructure/ExternalChatbotClientByWebFlux.java @@ -0,0 +1,44 @@ +package dododocs.dododocs.chatbot.infrastructure; + +import dododocs.dododocs.analyze.exception.NoExistGitRepoException; +import dododocs.dododocs.analyze.exception.NoExistRepoAnalyzeException; +import dododocs.dododocs.chatbot.dto.ExternalQuestToChatbotRequest; +import dododocs.dododocs.chatbot.dto.ExternalQuestToChatbotResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@Component +public class ExternalChatbotClientByWebFlux { + private final WebClient webClient; + private final String aiBasicUrl; + private static final String CHATBOT_QUESTION_REQUEST_URL_PREFIX = "/chat"; + + public ExternalChatbotClientByWebFlux(@Value("${ai.basic_url}") final String aiBasicUrl, WebClient.Builder webClientBuilder) { + this.aiBasicUrl = aiBasicUrl; + this.webClient = WebClient.builder() + .baseUrl(aiBasicUrl) + .defaultHeader(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM_VALUE) // SSE 헤더 + .build(); + } + + public Mono questToChatbot(final ExternalQuestToChatbotRequest questToChatbotRequest) { + System.out.println("==========qweqweqwe"); + return webClient.method(HttpMethod.GET) + .uri(aiBasicUrl + CHATBOT_QUESTION_REQUEST_URL_PREFIX) + .bodyValue(questToChatbotRequest) + .retrieve() + .bodyToMono(ExternalQuestToChatbotResponse.class) + .onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + System.out.println(throwable.getMessage()); + return new NoExistRepoAnalyzeException("레포지토리 결과물을 아직 생성중입니다. 잠시만 기다려주세요."); + } + return new NoExistGitRepoException("레포지토리 결과물을 아직 생성중입니다. 잠시만 기다려주세요."); + }); + } +} diff --git a/src/main/java/dododocs/dododocs/chatbot/presentation/ChatbotController.java b/src/main/java/dododocs/dododocs/chatbot/presentation/ChatbotController.java new file mode 100644 index 0000000..484ff94 --- /dev/null +++ b/src/main/java/dododocs/dododocs/chatbot/presentation/ChatbotController.java @@ -0,0 +1,207 @@ +package dododocs.dododocs.chatbot.presentation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import dododocs.dododocs.analyze.domain.repository.RepoAnalyzeRepository; +import dododocs.dododocs.analyze.exception.NoExistRepoAnalyzeException; +import dododocs.dododocs.auth.dto.Accessor; +import dododocs.dododocs.auth.presentation.authentication.Authentication; +import dododocs.dododocs.chatbot.application.ChatbotService; +import dododocs.dododocs.chatbot.domain.ChatLog; +import dododocs.dododocs.chatbot.domain.repository.ChatLogRepository; +import dododocs.dododocs.chatbot.dto.*; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.data.domain.Pageable; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/chatbot") +public class ChatbotController { + private final ChatbotService chatbotService; + private final ChatLogRepository chatLogRepository; + private final RepoAnalyzeRepository repoAnalyzeRepository; + private final WebClient webClient; + + @Value("${ai.basic_url}") + private String aiBasicUrl; + + + @PostMapping("/question/save/{registeredRepoId}") + public ExternalQuestToChatbotResponse questionToChatbotAndSaveLogs(@PathVariable final Long registeredRepoId, + @RequestBody final QuestToChatbotRequest questToChatbotRequest) { + return chatbotService.questionToChatbotAndSaveLogs(registeredRepoId, questToChatbotRequest.getQuestion()); + } + + @GetMapping("/logs/{registeredRepoId}") + public ResponseEntity findChatbotHistory(@Authentication final Accessor accessor, + @PathVariable final Long registeredRepoId, + @PageableDefault(size = 30) final Pageable pageable) { + return ResponseEntity.ok(chatbotService.findChatbotHistory(registeredRepoId, pageable)); + } + + /* @GetMapping(value = "/stream-from-backend", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux streamFromBackend() { + System.out.println("AI 서버로 데이터 요청 시작..."); + System.out.println(aiBasicUrl); + return webClient.get() + .uri("/chat") + .retrieve() + .bodyToFlux(String.class) // SSE 데이터를 Flux로 수신 + .map(data -> { + System.out.println("AI 서버에서 수신한 데이터: " + data); + return new TestWebFluxResponse(data); + }) + .doOnError(error -> System.err.println("AI 서버 연결 에러: " + error.getMessage())) + .onErrorResume(error -> Flux.just(new TestWebFluxResponse("AI 서버와 연결 중 문제가 발생했습니다."))); + } */ + + @GetMapping(value = "/stream-and-save-test", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux streamAndSaveChatLogsTest() { + final RepoAnalyze repoAnalyze = repoAnalyzeRepository.findById(1L) + .orElseThrow(() -> new NoExistRepoAnalyzeException("레포지토리 정보가 존재하지 않습니다.")); + + StringBuilder aggregatedText = new StringBuilder(); + + return webClient.get() + .uri("/chat22222222") + .retrieve() + .bodyToFlux(String.class) + .map(data -> { + System.out.println("AI 서버에서 수신한 데이터: " + data); + aggregatedText.append(data).append(" "); + return new TestWebFluxResponse(data); + }) + .doOnComplete(() -> { + String aggregatedResult = aggregatedText.toString().trim(); + chatLogRepository.save(new ChatLog("Aggregated Question", aggregatedResult, repoAnalyze)); + System.out.println("전체 텍스트 저장 완료: " + aggregatedResult); + }) + .doOnError(error -> System.err.println("AI 서버 연결 에러: " + error.getMessage())) + .onErrorResume(error -> Flux.just(new TestWebFluxResponse("AI 서버와 연결 중 문제가 발생했습니다."))); + } + + /* @PostMapping(value = "/stream-and-save/{registeredRepoId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux streamAndSaveChatLogs(@PathVariable final Long registeredRepoId, + @RequestBody final QuestToChatbotRequest questToChatbotRequest) { + final RepoAnalyze repoAnalyze = repoAnalyzeRepository.findById(registeredRepoId) + .orElseThrow(() -> new NoExistRepoAnalyzeException("레포지토리 정보가 존재하지 않습니다.")); + + final List chatLogs = chatLogRepository.findTop3ByRepoAnalyzeOrderBySequenceDesc(repoAnalyze); + final List recentChatLogs = chatLogs.stream() + .map(chatLog -> new ExternalQuestToChatbotRequest.RecentChatLog(chatLog.getQuestion(), chatLog.getAnswer())) + .toList(); + + System.out.println("레포 URL: " + repoAnalyze.getRepoUrl()); + + final ExternalQuestToChatbotRequest externalQuestToChatbotRequest = new ExternalQuestToChatbotRequest( + repoAnalyze.getRepoUrl(), + questToChatbotRequest.getQuestion(), + recentChatLogs, + true + ); + + StringBuilder aggregatedText = new StringBuilder(); + + return webClient.post() + .uri("/chat") + .bodyValue(externalQuestToChatbotRequest) + .retrieve() + .bodyToFlux(String.class) // 응답을 문자열로 받습니다. + .map(data -> { + System.out.println("AI 서버에서 수신한 데이터: " + data); + + // JSON 데이터에서 "answer" 키의 값을 추출 + String extractedAnswer; + try { + JsonNode jsonNode = new ObjectMapper().readTree(data); + extractedAnswer = jsonNode.get("answer").asText(); + } catch (JsonProcessingException e) { + System.out.println("JSON 파싱 오류: " + e.getMessage()); + throw new RuntimeException("응답 데이터 파싱 중 오류 발생", e); + } + + aggregatedText.append(extractedAnswer).append(" "); + return extractedAnswer; // 파싱된 값 반환 + }) + .doOnComplete(() -> { + String aggregatedResult = aggregatedText.toString().trim(); + chatLogRepository.save(new ChatLog(questToChatbotRequest.getQuestion(), aggregatedResult, repoAnalyze)); + System.out.println("전체 텍스트 저장 완료: " + aggregatedResult); + }) + .doOnError(error -> System.out.println("AI 서버 연결 에러: " + error.getMessage())) + .onErrorResume(error -> { + System.out.println("AI 서버와 연결 중 문제가 발생했습니다."); + return Flux.just("AI 서버와 연결 중 문제가 발생했습니다."); + }); + } */ + + @PostMapping(value = "/stream-and-save/{registeredRepoId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux streamAndSaveChatLogs(@PathVariable final Long registeredRepoId, + @RequestBody final QuestToChatbotRequest questToChatbotRequest) { + final RepoAnalyze repoAnalyze = repoAnalyzeRepository.findById(registeredRepoId) + .orElseThrow(() -> new NoExistRepoAnalyzeException("레포지토리 정보가 존재하지 않습니다.")); + + final List chatLogs = chatLogRepository.findTop3ByRepoAnalyzeOrderBySequenceDesc(repoAnalyze); + final List recentChatLogs = chatLogs.stream() + .map(chatLog -> new ExternalQuestToChatbotRequest.RecentChatLog(chatLog.getQuestion(), chatLog.getAnswer())) + .toList(); + + System.out.println("레포 URL: " + repoAnalyze.getRepoUrl()); + + final ExternalQuestToChatbotRequest externalQuestToChatbotRequest = new ExternalQuestToChatbotRequest( + repoAnalyze.getRepoUrl(), + questToChatbotRequest.getQuestion(), + recentChatLogs, + true + ); + + StringBuilder aggregatedText = new StringBuilder(); + + return webClient.post() + .uri("/chat") + .bodyValue(externalQuestToChatbotRequest) + .retrieve() + .bodyToFlux(String.class) + .map(data -> { + System.out.println("AI 서버에서 수신한 데이터: " + data); + + // JSON 데이터에서 "answer" 키의 값을 추출 + String extractedAnswer; + try { + JsonNode jsonNode = new ObjectMapper().readTree(data); + extractedAnswer = jsonNode.get("answer").asText(); + } catch (JsonProcessingException e) { + System.out.println("JSON 파싱 오류: " + e.getMessage()); + throw new RuntimeException("응답 데이터 파싱 중 오류 발생", e); + } + + aggregatedText.append(extractedAnswer).append(" "); + return new ExternalQuestToChatbotResponse(extractedAnswer); // DTO로 변환 + }) + .doOnComplete(() -> { + String aggregatedResult = aggregatedText.toString().trim(); + chatLogRepository.save(new ChatLog(questToChatbotRequest.getQuestion(), aggregatedResult, repoAnalyze)); + System.out.println("전체 텍스트 저장 완료: " + aggregatedResult); + }) + .doOnError(error -> System.out.println("AI 서버 연결 에러: " + error.getMessage())) + .onErrorResume(error -> { + System.out.println("AI 서버와 연결 중 문제가 발생했습니다."); + return Flux.just(new ExternalQuestToChatbotResponse("AI 서버와 연결 중 문제가 발생했습니다.")); + }); + } + +} diff --git a/src/main/java/dododocs/dododocs/global/BaseEntity.java b/src/main/java/dododocs/dododocs/global/BaseEntity.java new file mode 100644 index 0000000..4bc082e --- /dev/null +++ b/src/main/java/dododocs/dododocs/global/BaseEntity.java @@ -0,0 +1,26 @@ +package dododocs.dododocs.global; + +import jakarta.persistence.Column; +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 org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDate createdAt; + + @LastModifiedDate + @Column(name = "updated_at", nullable = false) + private LocalDate updatedAt; +} \ No newline at end of file diff --git a/src/main/java/dododocs/dododocs/global/GlobalExceptionHandler.java b/src/main/java/dododocs/dododocs/global/GlobalExceptionHandler.java index 4c5245d..736ea1c 100644 --- a/src/main/java/dododocs/dododocs/global/GlobalExceptionHandler.java +++ b/src/main/java/dododocs/dododocs/global/GlobalExceptionHandler.java @@ -1,5 +1,8 @@ package dododocs.dododocs.global; +import dododocs.dododocs.analyze.exception.NoExistGitRepoException; +import dododocs.dododocs.analyze.exception.NoExistRepoAnalyzeException; +import dododocs.dododocs.auth.exception.InvalidTokenException; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,6 +20,25 @@ public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + @ExceptionHandler({ + NoExistGitRepoException.class, + NoExistRepoAnalyzeException.class, + }) + public ResponseEntity handleNotFoundException(final RuntimeException e) { + logger.error(e.getMessage(), e); + ExceptionResponse exceptionResponse = new ExceptionResponse(e.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exceptionResponse); + } + + @ExceptionHandler({ + InvalidTokenException.class + }) + public ResponseEntity handleUnAuthorizedException(final RuntimeException e) { + logger.error(e.getMessage(), e); + ExceptionResponse exceptionResponse = new ExceptionResponse(e.getMessage()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(exceptionResponse); + } + @ExceptionHandler({ RuntimeException.class }) @@ -25,6 +47,8 @@ public ResponseEntity handleIBadRequestException(final Runtim ExceptionResponse exceptionResponse = new ExceptionResponse(e.getMessage()); return ResponseEntity.badRequest().body(exceptionResponse); } + + @ExceptionHandler({ NoResourceFoundException.class }) diff --git a/src/main/java/dododocs/dododocs/global/config/CorsConfig.java b/src/main/java/dododocs/dododocs/global/config/CorsConfig.java index 0fa28bf..5480d68 100644 --- a/src/main/java/dododocs/dododocs/global/config/CorsConfig.java +++ b/src/main/java/dododocs/dododocs/global/config/CorsConfig.java @@ -11,10 +11,14 @@ public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(final CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000", "http://localhost:8000", "https://kcfaa61d53ebba.user-app.krampoline.com", "https://kcfaa61d53ebba.user-app.krampoline.com:3000") + .allowedOrigins("http://localhost:3000", "http://localhost:8000", "http://localhost:8080", "http://127.0.0.1:5500", + "http://ai.dododocs.com:5000", + "http://ai.dododocs.com:8000", + "https://dododocs.com", "https://dododocs.com:3000", "https://dododocs.com:443", + "http://ai.dododocs.com:5001", + "https://kcfaa61d53ebba.user-app.krampoline.com", "https://kcfaa61d53ebba.user-app.krampoline.com:3000") .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") - .allowCredentials(true) - .exposedHeaders(HttpHeaders.LOCATION); - WebMvcConfigurer.super.addCorsMappings(registry); + .allowedHeaders("*") // 모든 헤더 허용 + .allowCredentials(true); } } \ No newline at end of file diff --git a/src/main/java/dododocs/dododocs/global/config/JacksonConfig.java b/src/main/java/dododocs/dododocs/global/config/JacksonConfig.java new file mode 100644 index 0000000..dd7c30c --- /dev/null +++ b/src/main/java/dododocs/dododocs/global/config/JacksonConfig.java @@ -0,0 +1,23 @@ +package dododocs.dododocs.global.config; + + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@Configuration +public class JacksonConfig { + + @Bean + @Primary // Spring의 기본 ObjectMapper를 대체 + public ObjectMapper objectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); // JavaTimeModule 등록 + return mapper; + } +} diff --git a/src/main/java/dododocs/dododocs/global/config/JpaAuditConfig.java b/src/main/java/dododocs/dododocs/global/config/JpaAuditConfig.java new file mode 100644 index 0000000..916660e --- /dev/null +++ b/src/main/java/dododocs/dododocs/global/config/JpaAuditConfig.java @@ -0,0 +1,9 @@ +package dododocs.dododocs.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaAuditConfig { +} diff --git a/src/main/java/dododocs/dododocs/global/config/RestTemplateConfig.java b/src/main/java/dododocs/dododocs/global/config/RestTemplateConfig.java index 700512d..aad928b 100644 --- a/src/main/java/dododocs/dododocs/global/config/RestTemplateConfig.java +++ b/src/main/java/dododocs/dododocs/global/config/RestTemplateConfig.java @@ -1,8 +1,8 @@ package dododocs.dododocs.global.config; - import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @@ -13,7 +13,8 @@ public class RestTemplateConfig { @Bean - public RestTemplate restTemplate() { + @Profile({"dev"}) + public RestTemplate restTemplateWithProxy() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("krmp-proxy.9rum.cc", 3128)); @@ -21,4 +22,10 @@ public RestTemplate restTemplate() { return new RestTemplate(factory); } -} \ No newline at end of file + + @Bean + @Profile({"default", "local", "test", "prod"}) + public RestTemplate simpleRestTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/dododocs/dododocs/global/config/S3Config.java b/src/main/java/dododocs/dododocs/global/config/S3Config.java index 49705e0..feaa467 100644 --- a/src/main/java/dododocs/dododocs/global/config/S3Config.java +++ b/src/main/java/dododocs/dododocs/global/config/S3Config.java @@ -1,5 +1,6 @@ package dododocs.dododocs.global.config; +import com.amazonaws.ClientConfiguration; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.services.s3.AmazonS3Client; @@ -7,20 +8,44 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; @Configuration public class S3Config { - @Value("${cloud.aws.credentials.access-key}") + @Value("${cloud.aws.credentials.accessKey}") private String accessKey; - @Value("${cloud.aws.credentials.secret-key}") + @Value("${cloud.aws.credentials.secretKey}") private String secretKey; @Value("${cloud.aws.region.static}") private String region; @Bean - public AmazonS3Client amazonS3Client() { + @Profile({"dev"}) + public AmazonS3Client amazonS3ClientWithProxy() { + // AWS 인증 정보 생성 BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey); + + // 프록시 설정 추가 + ClientConfiguration clientConfiguration = new ClientConfiguration(); + clientConfiguration.setProxyHost("krmp-proxy.9rum.cc"); + clientConfiguration.setProxyPort(3128); + + // S3 클라이언트 생성 + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials)) + .withClientConfiguration(clientConfiguration) + .build(); + } + + @Bean + @Profile({"default", "local", "test", "prod"}) + public AmazonS3Client simpleAmazonS3Client() { + // AWS 인증 정보 생성 + BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey); + + // 기본 S3 클라이언트 생성 (프록시 없음) return (AmazonS3Client) AmazonS3ClientBuilder.standard() .withRegion(region) .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials)) diff --git a/src/main/java/dododocs/dododocs/global/config/WebClientConfig.java b/src/main/java/dododocs/dododocs/global/config/WebClientConfig.java new file mode 100644 index 0000000..aae5906 --- /dev/null +++ b/src/main/java/dododocs/dododocs/global/config/WebClientConfig.java @@ -0,0 +1,47 @@ +package dododocs.dododocs.global.config; + +import io.netty.handler.proxy.ProxyHandler; +import io.netty.handler.proxy.HttpProxyHandler; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; +import reactor.netty.transport.ProxyProvider; + +import java.net.InetSocketAddress; + +@Configuration +public class WebClientConfig { + + @Value("${ai.basic_url}") + private String aiBasicUrl; + + @Bean + @Profile({"dev"}) + public WebClient webClientWithProxy() { + InetSocketAddress proxyAddress = new InetSocketAddress("krmp-proxy.9rum.cc", 3128); + + return WebClient.builder() + .baseUrl(aiBasicUrl) + .defaultHeader(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM_VALUE) + .clientConnector(new ReactorClientHttpConnector(HttpClient.create() + .proxy(proxy -> proxy.type(ProxyProvider.Proxy.HTTP).address(proxyAddress)))) + .build(); + } + + @Bean + @Profile({"default", "local", "test", "prod"}) + public WebClient simpleWebClient() { + return WebClient.builder() + .baseUrl(aiBasicUrl) + .defaultHeader(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM_VALUE) + .build(); + } +} + diff --git a/src/main/java/dododocs/dododocs/member/application/MemberService.java b/src/main/java/dododocs/dododocs/member/application/MemberService.java index 651b994..39f1975 100644 --- a/src/main/java/dododocs/dododocs/member/application/MemberService.java +++ b/src/main/java/dododocs/dododocs/member/application/MemberService.java @@ -2,40 +2,176 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import dododocs.dododocs.analyze.application.AnalyzeService; +import dododocs.dododocs.analyze.application.DownloadFromS3Service; +import dododocs.dododocs.analyze.domain.MemberOrganization; +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import dododocs.dododocs.analyze.domain.repository.MemberOrganizationRepository; +import dododocs.dododocs.analyze.domain.repository.RepoAnalyzeRepository; +import dododocs.dododocs.analyze.dto.EmptyFolderException; +import dododocs.dododocs.analyze.exception.NoExistRepoAnalyzeException; import dododocs.dododocs.auth.domain.repository.MemberRepository; import dododocs.dododocs.auth.exception.NoExistMemberException; +import dododocs.dododocs.chatbot.application.ChatbotService; +import dododocs.dododocs.chatbot.dto.FindMemberInfoResponse; import dododocs.dododocs.member.domain.Member; +import dododocs.dododocs.member.dto.FindRegisterMemberRepoResponses; +import dododocs.dododocs.member.dto.FindRegisterRepoResponse; import dododocs.dododocs.member.dto.FindRepoNameListResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -@Transactional @RequiredArgsConstructor +// @Transactional(readOnly = true) @Service public class MemberService { private final MemberRepository memberRepository; - private final RestTemplate restTemplate = new RestTemplate(); - private final ObjectMapper objectMapper = new ObjectMapper(); + private final MemberOrganizationRepository memberOrganizationRepository; + private final RepoAnalyzeRepository repoAnalyzeRepository; + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + + private final DownloadFromS3Service downloadFromS3Service; // downloadAndProcessZipReadmeInfo, downloadAndProcessZipDocsInfo + private final ChatbotService chatbotService; // questionToChatbotAndSaveLogs public FindRepoNameListResponse getUserRepositories(final long memberId) { final Member member = memberRepository.findById(memberId) .orElseThrow(NoExistMemberException::new); + final String memberName = member.getOriginName(); final String url = "https://api.github.com/users/" + memberName + "/repos"; + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + member.getAccessToken()); + HttpEntity entity = new HttpEntity<>(headers); + // GitHub API 호출하여 레포지토리 목록 가져오기 - Object response = restTemplate.getForObject(url, Object.class); + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.GET, entity, Object.class); + Object response = responseEntity.getBody(); List> repos = objectMapper.convertValue(response, new TypeReference>>() {}); - return new FindRepoNameListResponse(repos.stream() - .map(repo -> (String) repo.get("name")) - .collect(Collectors.toList())); + final List memberOrganizations = memberOrganizationRepository.findOrganizationNamesByMember(member); + + // 조직 이름으로도 레포지토리 검색 + List> organizationRepos = memberOrganizations.stream() + .flatMap(orgName -> { + String orgUrl = "https://api.github.com/orgs/" + orgName + "/repos"; + ResponseEntity orgResponseEntity = restTemplate.exchange(orgUrl, HttpMethod.GET, entity, Object.class); + Object orgResponse = orgResponseEntity.getBody(); + return objectMapper.convertValue(orgResponse, new TypeReference>>() {}).stream(); + }) + .collect(Collectors.toList()); + + // 사용자 레포와 조직 레포 병합 + List> combinedRepos = new ArrayList<>(repos); + combinedRepos.addAll(organizationRepos); + + return new FindRepoNameListResponse(combinedRepos.stream() + .map(repo -> (String) repo.get("name")) + .distinct() + .collect(Collectors.toList())); + } + + public FindRegisterMemberRepoResponses findRegisterMemberRepoResponses(final long memberId) { + final Member member = memberRepository.findById(memberId) + .orElseThrow(NoExistMemberException::new); + + final List repoAnalyzes = repoAnalyzeRepository.findByMember(member); + return new FindRegisterMemberRepoResponses(findRepoRegisteredCompleteStatus(repoAnalyzes)); + } + + private List findRepoRegisteredCompleteStatus(final List repoAnalyzes) { + List findRegisterRepoResponses = new ArrayList<>(); + + for (final RepoAnalyze repoAnalyze : repoAnalyzes) { + final long registeredRepoId = repoAnalyze.getId(); + + FindRegisterRepoResponse response = new FindRegisterRepoResponse(repoAnalyze); + + response.setReadmeComplete(repoAnalyze.isReadmeCompleted()); + response.setDocsComplete(repoAnalyze.isDocsCompleted()); + response.setChatbotComplete(repoAnalyze.isChatbotCompleted()); + + findRegisterRepoResponses.add(response); + } + + return findRegisterRepoResponses; + } + + + /* private List findRepoRegisteredCompleteStatus(final List repoAnalyzes) { + List findRegisterRepoResponses = new ArrayList<>(); + + for (final RepoAnalyze repoAnalyze : repoAnalyzes) { + final long registeredRepoId = repoAnalyze.getId(); + FindRegisterRepoResponse response = new FindRegisterRepoResponse(repoAnalyze); + + if(!repoAnalyze.isAnalyzed()) { + try { + chatbotService.questionToChatbotAndSaveLogs(registeredRepoId, "레포지토리 정보 좀 요약해서 알려줄래?"); + response.setChatbotComplete(true); + } catch (RuntimeException e) { + response.setChatbotComplete(false); + } + + System.out.println("===========1111"); + + try { + downloadFromS3Service.downloadAndProcessZipReadmeInfo(registeredRepoId); + response.setReadmeComplete(true); + } catch (Exception e) { + response.setReadmeComplete(false); + } + + System.out.println("===========2222"); + + try { + downloadFromS3Service.downloadAndProcessZipDocsInfoTest(registeredRepoId); + response.setDocsComplete(true); + } catch (EmptyFolderException e) { + response.setDocsComplete(true); + } catch (Exception e) { + response.setDocsComplete(false); + } + + System.out.println("===========3333"); + + if(response.isDocsComplete() && response.isReadmeComplete() && response.isChatbotComplete()) { + System.out.println("wwwwwwwwwwwwwwwwwqwoijejqoiwfnnfnfnfnfnfnwq"); + repoAnalyze.setAnalyzed(true); + repoAnalyzeRepository.save(repoAnalyze); + } + + findRegisterRepoResponses.add(response); + } else { + System.out.println("=========gtyewrwerwerwebbggg"); + response.setDocsComplete(true); + response.setReadmeComplete(true); + response.setChatbotComplete(true); + findRegisterRepoResponses.add(response); + } + } + + return findRegisterRepoResponses; + } */ + + + public FindMemberInfoResponse findMemberInfo(final long memberId) { + final Member member = memberRepository.findById(memberId) + .orElseThrow(NoExistMemberException::new); + + return new FindMemberInfoResponse(member.getOriginName()); } } diff --git a/src/main/java/dododocs/dododocs/member/domain/Member.java b/src/main/java/dododocs/dododocs/member/domain/Member.java index 7bbfa16..99a9d36 100644 --- a/src/main/java/dododocs/dododocs/member/domain/Member.java +++ b/src/main/java/dododocs/dododocs/member/domain/Member.java @@ -2,7 +2,9 @@ import jakarta.persistence.*; import lombok.Getter; +import lombok.Setter; +@Setter @Getter @Table(name = "member") @Entity @@ -12,8 +14,8 @@ public class Member { @Column(name = "id") private Long id; - @Column(name = "email") - private String email; + @Column(name = "social_login_id") + private Long socialLoginId; @Column(name = "nick_name") private String nickname; @@ -21,17 +23,29 @@ public class Member { @Column(name = "origin_name") private String originName; + @Column(name = "access_token") + private String accessToken; + + protected Member() { } - public Member(final String email, final String nickname, final String originName) { - this.email = email; + public Member(final Long socialLoginId, final String nickname, final String originName, final String accessToken) { + this.socialLoginId = socialLoginId; + this.nickname = nickname; + this.originName = originName; + this.accessToken = accessToken; + } + + + public Member(final Long socialLoginId, final String nickname, final String originName) { + this.socialLoginId = socialLoginId; this.nickname = nickname; this.originName = originName; } public Member(final String email) { - this.email = "devhaon@kakao.com"; + this.socialLoginId = 123123123L; this.nickname = "devhaon"; this.originName = "lee min sung"; } diff --git a/src/main/java/dododocs/dododocs/member/dto/FindRegisterMemberRepoResponses.java b/src/main/java/dododocs/dododocs/member/dto/FindRegisterMemberRepoResponses.java new file mode 100644 index 0000000..bcebe41 --- /dev/null +++ b/src/main/java/dododocs/dododocs/member/dto/FindRegisterMemberRepoResponses.java @@ -0,0 +1,21 @@ +package dododocs.dododocs.member.dto; + +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.stream.Collectors; + +@Getter +public class FindRegisterMemberRepoResponses { + private List findRegisterRepoResponses; + + private FindRegisterMemberRepoResponses() { + } + + public FindRegisterMemberRepoResponses(final List findRegisterMemberRepoResponses) { + this.findRegisterRepoResponses = findRegisterMemberRepoResponses; + } +} diff --git a/src/main/java/dododocs/dododocs/member/dto/FindRegisterRepoResponse.java b/src/main/java/dododocs/dododocs/member/dto/FindRegisterRepoResponse.java new file mode 100644 index 0000000..d6ad3d0 --- /dev/null +++ b/src/main/java/dododocs/dododocs/member/dto/FindRegisterRepoResponse.java @@ -0,0 +1,40 @@ +package dododocs.dododocs.member.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Setter +@Getter +public class FindRegisterRepoResponse { + private Long registeredRepoId; + private String repositoryName; + private String branchName; + + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate createdAt; + private boolean readmeComplete; + private boolean chatbotComplete; + private boolean docsComplete; + + private FindRegisterRepoResponse() { + } + + public FindRegisterRepoResponse(final RepoAnalyze repoAnalyze) { + this.registeredRepoId = repoAnalyze.getId(); + this.repositoryName = repoAnalyze.getRepositoryName(); + this.branchName = repoAnalyze.getBranchName(); + + if(repoAnalyze.getCreatedAt() != null) { + System.out.println("qqwihueqwhiuehqiwuehiuqwheiuqwhgiuyeqhgwiuyehiuqwhiueqwe"); + this.createdAt = LocalDate.from(repoAnalyze.getCreatedAt()); + } else { + System.out.println("ojiqwejqwjioejqiowejioqwjeoiqwjoievn"); + this.createdAt = LocalDate.now(); + } + } +} diff --git a/src/main/java/dododocs/dododocs/member/presentation/MemberController.java b/src/main/java/dododocs/dododocs/member/presentation/MemberController.java index 12fd994..a8a4096 100644 --- a/src/main/java/dododocs/dododocs/member/presentation/MemberController.java +++ b/src/main/java/dododocs/dododocs/member/presentation/MemberController.java @@ -2,15 +2,15 @@ import dododocs.dododocs.auth.dto.Accessor; import dododocs.dododocs.auth.presentation.authentication.Authentication; +import dododocs.dododocs.chatbot.dto.FindMemberInfoResponse; import dododocs.dododocs.member.application.MemberService; +import dododocs.dododocs.member.dto.FindRegisterMemberRepoResponses; import dododocs.dododocs.member.dto.FindRepoNameListResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - @RestController @RequestMapping("/api/member") public class MemberController { @@ -20,9 +20,19 @@ public MemberController(final MemberService memberService) { this.memberService = memberService; } - @GetMapping("/repos") - public ResponseEntity findMemberRepoList(@Authentication final Accessor accessor) { + @GetMapping("/repos/all") + public ResponseEntity findMemberRepoAllList(@Authentication final Accessor accessor) { FindRepoNameListResponse findRepoNameListResponse = memberService.getUserRepositories(accessor.getId()); return ResponseEntity.ok(findRepoNameListResponse); } + + @GetMapping("/repos/registered") + public ResponseEntity findMemberRepoRegisteredList(@Authentication final Accessor accessor) { + return ResponseEntity.ok(memberService.findRegisterMemberRepoResponses(accessor.getId())); + } + + @GetMapping("/info") + public ResponseEntity findMemberInfo(@Authentication final Accessor accessor) { + return ResponseEntity.ok(memberService.findMemberInfo(accessor.getId())); + } } diff --git a/src/main/java/dododocs/dododocs/test/ApiTestController.java b/src/main/java/dododocs/dododocs/test/ApiTestController.java index b2e9aef..0ef3be9 100644 --- a/src/main/java/dododocs/dododocs/test/ApiTestController.java +++ b/src/main/java/dododocs/dododocs/test/ApiTestController.java @@ -1,23 +1,31 @@ package dododocs.dododocs.test; +import dododocs.dododocs.analyze.dto.DownloadAiAnalyzeRequest; +import dododocs.dododocs.analyze.dto.DownloadAiAnalyzeResponse; import dododocs.dododocs.auth.domain.repository.MemberRepository; +import dododocs.dododocs.auth.dto.Accessor; +import dododocs.dododocs.auth.presentation.authentication.Authentication; import dododocs.dododocs.member.domain.Member; import dododocs.dododocs.test.dto.CreateMemberRequest; import dododocs.dododocs.test.dto.FindDbTestResponse; import dododocs.dododocs.test.dto.FindTrueTestResponse; +import dododocs.dododocs.test.infrastructure.ExternalAiTestClient; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; @RequestMapping("/api") @RestController public class ApiTestController { + private final ExternalAiTestClient externalAiTestClient; private final MemberRepository memberRepository; - public ApiTestController(final MemberRepository memberRepository) { + public ApiTestController(final MemberRepository memberRepository, + final ExternalAiTestClient externalAiTestClient) { this.memberRepository = memberRepository; + this.externalAiTestClient = externalAiTestClient; } @GetMapping("/server") @@ -34,7 +42,7 @@ public ResponseEntity findDBResponse() { public ResponseEntity insertMember() { Member member = memberRepository.save(new Member("email")); System.out.println(member.getId()); - System.out.println(member.getEmail()); + System.out.println(member.getSocialLoginId()); return ResponseEntity.noContent().build(); } @@ -43,8 +51,263 @@ public ResponseEntity findMember() { return ResponseEntity.ok(memberRepository.findById(1L).orElse(null)); } - @GetMapping("/proxy") - public ResponseEntity findProxy() { - return ResponseEntity.ok().build(); + @GetMapping("/ping") + public ResponseEntity ping() { + return ResponseEntity.ok(externalAiTestClient.requestTestAI()); + } + + @GetMapping("/analyze/result") + public ResponseEntity analyzeResultTest(@RequestParam final String repositoryName) { + List summaryFiles = List.of( + new DownloadAiAnalyzeResponse.FileDetail("kakao-25_moheng.README.md", """ + # Project Name + moheng + + ## Table of Contents + [ 📝 Overview](#📝-overview) \s + [ 📁 Project Structure](#📁-project-structure) \s + [ 🚀 Getting Started](#🚀-getting-started) \s + [ 💡 Motivation](#💡-motivation) \s + [ 🎬 Demo](#🎬-demo) \s + [ 🌐 Deployment](#🌐-deployment) \s + [ 🤝 Contributing](#🤝-contributing) \s + [ ❓ Troubleshooting & FAQ](#❓-troubleshooting-&-faq) \s + [ 📈 Performance](#📈-performance) \s + + ## 📝 Overview + 이 프로젝트는 여행 계획 및 추천 시스템을 구축하기 위한 것입니다. 사용자는 여행지에 대한 정보를 얻고, 개인화된 추천을 받을 수 있습니다. + + ### Main Purpose + - 사용자가 여행 계획을 세우고, 개인의 선호에 맞는 여행지를 추천받을 수 있도록 지원합니다. + - 여행지에 대한 정보를 제공하고, 사용자의 클릭 데이터를 기반으로 추천 알고리즘을 통해 맞춤형 여행지를 제안합니다. + + ### Key Features + - 사용자 맞춤형 여행지 추천 + - 여행지 정보 제공 + - 클릭 데이터를 기반으로 한 추천 알고리즘 + + ### Core Technology Stack + - Frontend: React, Vite + - Backend: Spring Boot + - Database: MySQL + - Others: Python, FastAPI (AI 모델 서빙) + + ## 📁 Project Structure + ``` + moheng + ├── 📁 ai + │ ├── 📁 model_serving + │ │ ├── 📁 application + │ │ ├── 📁 domain + │ │ ├── 📁 infra + │ │ ├── 📁 interface + │ │ └── ... + │ └── ... + ├── 📁 frontend + │ ├── 📁 src + │ │ ├── 📁 api + │ │ ├── 📁 components + │ │ └── ... + │ └── ... + ├── 📁 moheng + │ ├── 📁 auth + │ ├── 📁 planner + │ ├── 📁 trip + │ ├── 📁 member + │ └── ... + └── ... + ``` + + ## 🚀 Getting Started + + ### Prerequisites + + - 지원 운영 체제 + * Windows, macOS, Linux + - 필수 소프트웨어 + * Node.js (프론트엔드) + * Java (백엔드) + * Python 3.11 (AI) + - 버전 요구 사항 + * Node.js: 최신 안정 버전 + * Java: OpenJDK 22 + * Python: 3.11.x + - 패키지 관리자 + * npm (프론트엔드) + * Poetry (AI) + - 시스템 종속성 + * Docker + + ### Installation + + - Docker를 사용하여 환경을 설정할 수 있습니다. + + ```bash + # 리포지토리 클론 + git clone https://github.com/kakao-25/moheng.git + cd moheng-develop + + # Docker 설정을 위해 Docker가 시스템에 설치되고 실행 중인지 확인하십시오. + + # Docker 컨테이너 빌드 및 실행 + docker-compose up --build + + # 각 구성 요소를 별도로 빌드하고 실행하려면 아래 단계를 따르십시오: + + # 프론트엔드 설정 + cd frontend + docker build -t moheng-frontend -f Dockerfile.front . + docker run -p 3000:3000 moheng-frontend + + # 백엔드 설정 + cd backend + docker build -t moheng-backend -f Dockerfile.prod . + docker run -p 8080:8080 moheng-backend + + # AI/서비스 설정 + cd ai + docker build -t moheng-ai -f Dockerfile . + docker run -p 8000:8000 moheng-ai + + # Nginx 설정 + cd nginx + docker build -t moheng-nginx -f Dockerfile.prod . + docker run -p 80:80 moheng-nginx + + # 환경 구성 + # 각 구성 요소에 필요한 환경 변수가 설정되어 있는지 확인하십시오. + ``` + + ### Usage + + ```bash + # 실행 방법 + # Docker 컨테이너 설정 후, 다음 URL을 통해 서비스를 액세스하십시오: + + # 프론트엔드 + http://localhost:3000 + + # 백엔드 + http://localhost:8080 + + # AI 서비스 + http://localhost:8000 + + # Nginx (리버스 프록시 역할) + http://localhost + + + ## 💡 Motivation + 이 프로젝트는 여행 계획을 세우는 데 있어 사용자에게 더 나은 경험을 제공하기 위해 시작되었습니다. 개인의 선호를 반영한 추천 시스템을 통해 사용자가 더 쉽게 여행지를 선택할 수 있도록 돕고자 합니다. + + ## 🎬 Demo + ![Demo Video or Screenshot](path/to/demo.mp4) + + ## 🌐 Deployment + - AWS, Heroku와 같은 클라우드 플랫폼에 배포 가능 + - 배포 단계에 따라 환경 설정이 필요합니다. + + ## 🤝 Contributing + - 기여 방법: 이슈를 생성하거나 Pull Request를 통해 기여할 수 있습니다. + - 코드 표준: Java, Python, JavaScript의 일반적인 코딩 표준을 따릅니다. + - Pull Request 프로세스: 변경 사항을 설명하는 메시지와 함께 Pull Request를 제출합니다. + - 행동 강령: 모든 기여자는 존중과 배려의 태도로 참여해야 합니다. + + ## ❓ Troubleshooting & FAQ + - 자주 발생하는 문제 및 해결 방법을 문서화합니다. + - FAQ 섹션을 통해 사용자 질문에 대한 답변을 제공합니다. + + ## 📈 Performance + - 성능 벤치마크 및 최적화 기법을 문서화합니다. + - 시스템의 확장성 고려 사항을 포함합니다. + """) + ); + + List regularFiles = List.of( + new DownloadAiAnalyzeResponse.FileDetail("AuthController.md", """ + 아래는 제공된 Java 코드에 대한 아키텍처 문서입니다. 이 문서는 시스템의 구조와 각 컴포넌트의 역할을 설명합니다. + + # 시스템 아키텍처 문서 + + ## 전체 구조 + ```mermaid + graph TD + A[Client] --> B[AuthController] + B --> C[AuthService] + C --> D[TokenManager] + C --> E[MemberService] + D --> F[MemberToken] + E --> G[Member] + ``` + + ## 시스템 흐름 + ```mermaid + sequenceDiagram + Client->>AuthController: Request for URI + AuthController->>AuthService: generateUri() + AuthService-->>AuthController: Return URI + AuthController->>Client: Send URI + + Client->>AuthController: Login Request + AuthController->>AuthService: generateTokenWithCode() + AuthService-->>AuthController: Return MemberToken + AuthController->>Client: Send Access Token + + Client->>AuthController: Extend Login Request + AuthController->>AuthService: generateRenewalAccessToken() + AuthService-->>AuthController: Return RenewalAccessTokenResponse + AuthController->>Client: Send New Access Token + + Client->>AuthController: Logout Request + AuthController->>AuthService: removeRefreshToken() + AuthService-->>AuthController: Refresh Token Removed + AuthController->>Client: No Content Response + ``` + + ## 주요 컴포넌트 설명 + + ### AuthController + - **역할과 책임**: 클라이언트의 요청을 처리하고, 서비스 계층과의 상호작용을 통해 인증 관련 작업을 수행합니다. + - **주요 메서드**: + - `generateUri()`: OAuth 제공자로부터 인증 URI를 생성합니다. + - `login()`: OAuth 인증 코드를 사용하여 로그인하고, 액세스 토큰을 생성합니다. + - `extendLogin()`: 기존의 리프레시 토큰을 사용하여 새로운 액세스 토큰을 생성합니다. + - `logout()`: 리프레시 토큰을 제거하여 로그아웃을 처리합니다. + + ### AuthService + - **역할과 책임**: 인증 관련 비즈니스 로직을 처리합니다. OAuth 제공자와의 상호작용, 멤버 생성 및 토큰 관리를 담당합니다. + - **주요 메서드**: + - `generateTokenWithCode()`: OAuth 인증 코드를 사용하여 멤버 토큰을 생성합니다. + - `generateUri()`: OAuth 제공자로부터 URI를 생성합니다. + - `generateRenewalAccessToken()`: 리프레시 토큰을 사용하여 새로운 액세스 토큰을 생성합니다. + - `removeRefreshToken()`: 로그아웃 시 리프레시 토큰을 제거합니다. + - `extractMemberId()`: 액세스 토큰에서 멤버 ID를 추출합니다. + + ### TokenManager + - **역할과 책임**: 토큰 생성 및 검증을 담당합니다. 액세스 토큰과 리프레시 토큰의 생성 및 관리 기능을 제공합니다. + + ### MemberService + - **역할과 책임**: 멤버 관련 데이터의 CRUD 작업을 처리합니다. 멤버의 존재 여부를 확인하고, 새로운 멤버를 생성합니다. + + ### MemberToken + - **역할과 책임**: 액세스 토큰과 리프레시 토큰을 포함하는 데이터 구조입니다. 인증 과정에서 생성된 토큰 정보를 저장합니다. + + ## 주의사항 + 1. 각 컴포넌트의 역할과 책임을 명확히 이해하고, 필요 시 추가적인 설명을 덧붙이세요. + 2. 시스템의 흐름을 시퀀스 다이어그램으로 표현하여 클라이언트와 서버 간의 상호작용을 명확히 하세요. + 3. 각 메서드의 기능과 사용 예를 문서화하여 개발자들이 쉽게 이해할 수 있도록 하세요. + """) + ); + + return ResponseEntity.ok(new DownloadAiAnalyzeResponse(summaryFiles, regularFiles)); + } + + + + @PutMapping("/test/readme/update") + public ResponseEntity updateTestReadme(@RequestParam String repositoryName, + @RequestParam String fileName, + @RequestParam String newContent) throws Exception { + return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/dododocs/dododocs/test/infrastructure/ExternalAiTestClient.java b/src/main/java/dododocs/dododocs/test/infrastructure/ExternalAiTestClient.java new file mode 100644 index 0000000..45fb6c6 --- /dev/null +++ b/src/main/java/dododocs/dododocs/test/infrastructure/ExternalAiTestClient.java @@ -0,0 +1,51 @@ +package dododocs.dododocs.test.infrastructure; + +import dododocs.dododocs.analyze.dto.ExternalAiZipAnalyzeRequest; +import dododocs.dododocs.analyze.dto.ExternalAiZipAnalyzeResponse; +import dododocs.dododocs.auth.exception.NoExistMemberException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class ExternalAiTestClient { + private final String aiBasicUrl; + private final String AI_PING_TEST_URL_PREFIX = "/ping"; + private final RestTemplate restTemplate; + + public ExternalAiTestClient(final RestTemplate restTemplate, + @Value("${ai.basic_url}") final String aiBasicUrl) { + this.aiBasicUrl = aiBasicUrl; + this.restTemplate = restTemplate; + } + + public String requestTestAI() { + return requestAnalyze(); + } + + private String requestAnalyze() { + final Map uriVariables = new HashMap<>(); + + // Use an empty HttpEntity with no headers or body + final HttpEntity httpEntity = new HttpEntity<>(null); + + final ResponseEntity responseEntity = restTemplate.exchange( + aiBasicUrl + AI_PING_TEST_URL_PREFIX, + HttpMethod.GET, + httpEntity, + Void.class, + uriVariables + ); + + if (responseEntity.getStatusCode().is2xxSuccessful()) { + return "AI TEST SUCCESSFUL~~~~~~~~!!"; + } + return "AI TEST FAILED~~~~~~~!!"; + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..e5f1a04 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,59 @@ +spring: + jackson: + serialization: + write-dates-as-timestamps: false + application: + name: dododocs + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + connection-timeout: 180000 # 3 minutes (in milliseconds) + jpa: + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.MySQL8Dialect + show-sql: true + hibernate: + ddl-auto: create + open-in-view: false + level: + org: + hibernate: + type: + descriptor: + sql: + BasicBinder: TRACE +oauth: + github: + authorize_uri: ${AUTHORIZE_URI} + redirect_uri: ${REDIRECT_URI} + client_secret: ${CLIENT_SECRET} + token_uri: ${TOKEN_URL} + user_uri: ${USER_URI} + client_id: ${CLIENT_ID} + +security: + jwt: + token: + secret_key: secretsecretsecretsecretsecretsecret + expire_length: + access_token: 36000000 + refresh_token: 36000000 + +cloud: + aws: + s3: + bucket: haon-dododocs + stack.auto: false + region.static: ${AWS_REGION} + credentials: + accessKey: ${AWS_CREDEZNTIALS_ACCESSKEY} + secretKey: ${AWS_CREDEZNTIALS_SECRETKEY} + +ai: + basic_url: ${AI_BASIC_URL} + + diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..b37b325 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,57 @@ +spring: + jackson: + serialization: + write-dates-as-timestamps: false + application: + name: dododocs + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + jpa: + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.MySQL8Dialect + show-sql: true + hibernate: + ddl-auto: create + open-in-view: false + level: + org: + hibernate: + type: + descriptor: + sql: + BasicBinder: TRACE +oauth: + github: + authorize_uri: ${AUTHORIZE_URI} + redirect_uri: ${REDIRECT_URI} + client_secret: ${CLIENT_SECRET} + token_uri: ${TOKEN_URL} + user_uri: ${USER_URI} + client_id: ${CLIENT_ID} + +security: + jwt: + token: + secret_key: secretsecretsecretsecretsecretsecret + expire_length: + access_token: 36000000 + refresh_token: 36000000 + +cloud: + aws: + s3: + bucket: haon-dododocs + stack.auto: false + region.static: ${AWS_REGION} + credentials: + accessKey: ${AWS_CREDEZNTIALS_ACCESSKEY} + secretKey: ${AWS_CREDEZNTIALS_SECRETKEY} + +ai: + basic_url: ${AI_BASIC_URL} + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 34a6d7c..5733864 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,7 @@ spring: + jackson: + serialization: + write-dates-as-timestamps: false application: name: dododocs datasource: @@ -13,7 +16,7 @@ spring: dialect: org.hibernate.dialect.MySQL8Dialect show-sql: true hibernate: - ddl-auto: update + ddl-auto: create open-in-view: false level: org: @@ -49,5 +52,6 @@ cloud: accessKey: ${AWS_CREDEZNTIALS_ACCESSKEY} secretKey: ${AWS_CREDEZNTIALS_SECRETKEY} - +ai: + basic_url: ${AI_BASIC_URL} diff --git a/src/main/resources/static/docs/index.html b/src/main/resources/static/docs/index.html index 29fc641..0b58dba 100644 --- a/src/main/resources/static/docs/index.html +++ b/src/main/resources/static/docs/index.html @@ -451,11 +451,38 @@

📚️ DoDoDocs API 문서

  • ⛳️ Member (멤버) +
  • +
  • ⛳️ AI 문서화 + +
  • +
  • ⛳️ Chatbot (챗봇) + +
  • +
  • ⛳️ Test (테스트 API) +
  • @@ -481,6 +508,9 @@

    HTTP Response

    HTTP/1.1 200 OK
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
     Content-Type: application/json
     Content-Length: 47
     
    @@ -563,6 +593,9 @@ 

    HTTP Response

    HTTP/1.1 201 Created
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
     Content-Type: application/json
     Content-Length: 51
     
    @@ -597,6 +630,57 @@ 
    Response Body
    +
    +

    토큰 만료되었을 때 (로그아웃 해야할 때)

    +
    +

    HTTP Request

    +
    +
    +
    GET /api/auth/link HTTP/1.1
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Host: localhost:8080
    +
    +
    +
    +
    +

    Request Header

    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    엑세스 토큰

    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 401 Unauthorized
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +Content-Type: application/json
    +Content-Length: 66
    +
    +{
    +  "message" : "변조되었거나 만료된 토큰 입니다."
    +}
    +
    +
    +
    +
    @@ -605,10 +689,10 @@

    ⛳️ Member (멤버)

    멤버의 깃허브 레포지토리 이름 리스트 조회

    -

    HTTP Request

    +

    HTTP Request

    -
    GET /api/member/repos HTTP/1.1
    +
    GET /api/member/repos/all HTTP/1.1
     Content-Type: application/json;charset=UTF-8
     Authorization: Bearer aaaaaa.bbbbbb.cccccc
     Accept: application/json
    @@ -616,7 +700,7 @@ 

    HTTP Request

    -
    Request Header
    +
    Request Header
    @@ -646,10 +730,13 @@
    Request Body
    -

    HTTP Response

    +

    HTTP Response

    HTTP/1.1 200 OK
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
     Content-Type: application/json
     Content-Length: 60
     
    @@ -684,13 +771,1154 @@ 
    Response Body
    +
    +

    멤버 기본 프로필 정보 조회

    +
    +

    HTTP Request

    +
    +
    +
    GET /api/member/info HTTP/1.1
    +Content-Type: application/json;charset=UTF-8
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Accept: application/json
    +Host: localhost:8080
    +
    +
    +
    +
    Request Header
    +
    ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    엑세스 토큰

    +
    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 200 OK
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +Content-Type: application/json
    +Content-Length: 28
    +
    +{
    +  "nickname" : "devhaon"
    +}
    +
    +
    +
    +
    Response Fields
    + +++++ + + + + + + + + + + + + + + +
    PathTypeDescription

    nickname

    String

    깃허브 닉네임

    +
    +
    +
    +
    + +
    +

    ⛳️ AI 문서화

    +
    +
    +

    AI 분석 및 문서화 요청 (레포 등록)

    +
    +
    +
    POST /api/upload/s3 HTTP/1.1
    +Content-Type: application/json;charset=UTF-8
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Content-Length: 114
    +Host: localhost:8080
    +
    +{
    +  "repositoryName" : "Gatsby-Starter-Haon",
    +  "branchName" : "main",
    +  "korean" : true,
    +  "includeTest" : true
    +}
    +
    +
    +
    +

    HTTP Request

    +
    +
    +
    POST /api/upload/s3 HTTP/1.1
    +Content-Type: application/json;charset=UTF-8
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Content-Length: 114
    +Host: localhost:8080
    +
    +{
    +  "repositoryName" : "Gatsby-Starter-Haon",
    +  "branchName" : "main",
    +  "korean" : true,
    +  "includeTest" : true
    +}
    +
    +
    +
    +
    +

    Request Body

    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    repositoryName

    String

    분석 할 레포지토리 이름 (ex. Gatsby-Starter-Haon)

    branchName

    String

    브랜치 명 (ex. main)

    korean

    Boolean

    한국어 번역 여부

    includeTest

    Boolean

    테스트 포함 여부

    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 200 OK
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +
    +
    +
    +
    +
    +

    레포지토리 등록 실패 (존재하지 않는 (잘못된) 깃허브 레포지토리 URL 이나 브랜치명을 입력 받았을 때)

    +
    +

    HTTP Request

    +
    +
    +
    POST /api/upload/s3 HTTP/1.1
    +Content-Type: application/json;charset=UTF-8
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Accept: application/json
    +Content-Length: 119
    +Host: localhost:8080
    +
    +{
    +  "repositoryName" : "Invalid-Git-Repo-Name",
    +  "branchName" : "main222",
    +  "korean" : true,
    +  "includeTest" : true
    +}
    +
    +
    +
    +
    Request Body
    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    repositoryName

    String

    분석 할 레포지토리 이름 (ex. Gatsby-Starter-Haon)

    branchName

    String

    브랜치 명 (ex. main)

    korean

    Boolean

    한국어 번역 여부

    includeTest

    Boolean

    테스트 포함 여부

    +
    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 404 Not Found
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +Content-Type: application/json
    +Content-Length: 82
    +
    +{
    +  "message" : "존재하지 않는 레포지토리 또는 브랜치입니다."
    +}
    +
    +
    +
    +
    +
    +

    AI 분석 DOCS 결과 불러오기

    +
    +

    HTTP Request

    +
    +
    +
    POST /api/download/docs/1 HTTP/1.1
    +Content-Type: application/json;charset=UTF-8
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Accept: application/json
    +Host: localhost:8080
    +
    +
    +
    +
    +

    Path Variable

    + + ++++ + + + + + + + + + + + + +
    Table 1. /api/download/docs/{registeredRepoId}
    ParameterDescription

    registeredRepoId

    등록된 레포지토리 정보 고유 ID 값

    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 200 OK
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +Content-Type: application/json
    +Content-Length: 481
    +
    +{
    +  "summaryFiles" : [ {
    +    "fileName" : "Controller_Summary.md",
    +    "fileContents" : "전체 컨트롤러 요약 내용"
    +  }, {
    +    "fileName" : "Service_Summary.md",
    +    "fileContents" : "전체 서비스 요약 내용"
    +  } ],
    +  "regularFiles" : [ {
    +    "fileName" : "AuthController.md",
    +    "fileContents" : "설명1"
    +  }, {
    +    "fileName" : "AuthService.md",
    +    "fileContents" : "설명2"
    +  }, {
    +    "fileName" : "TravelController.md",
    +    "fileContents" : "설명3"
    +  } ]
    +}
    +
    +
    +
    +
    Response Body
    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    summaryFiles[]

    Array

    요약 파일 목록

    summaryFiles[].fileName

    String

    요약 파일 이름

    summaryFiles[].fileContents

    String

    요약 파일 내용

    regularFiles[]

    Array

    일반 파일 목록

    regularFiles[].fileName

    String

    일반 파일 이름

    regularFiles[].fileContents

    String

    일반 파일 내용

    +
    +
    +
    +
    +

    AI 분석 README 결과 불러오기

    +
    +

    HTTP Request

    +
    +
    +
    POST /api/download/readme/1 HTTP/1.1
    +Content-Type: application/json;charset=UTF-8
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Accept: application/json
    +Host: localhost:8080
    +
    +
    +
    +
    +

    Path Variable

    + + ++++ + + + + + + + + + + + + +
    Table 2. /api/download/readme/{registeredRepoId}
    ParameterDescription

    registeredRepoId

    등록된 레포지토리 정보 고유 ID 값

    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 200 OK
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +Content-Type: application/json
    +Content-Length: 57
    +
    +{
    +  "contents" : "AI 분석 결과 리드미 내용물"
    +}
    +
    +
    +
    +
    Response Body
    + +++++ + + + + + + + + + + + + + + +
    PathTypeDescription

    contents

    String

    리드미 내용

    +
    +
    +
    +
    +

    AI 분석 DOCS (또는 README, 챗봇 질문하기 등) 결과 불러오기 실패 (아직 AI 가 Document 생성을 완료하지 못한 경우)

    +
    +

    HTTP Request

    +
    +
    +
    POST /api/download/docs/1 HTTP/1.1
    +Content-Type: application/json;charset=UTF-8
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Accept: application/json
    +Host: localhost:8080
    +
    +
    +
    +
    +

    Path Variable

    + + ++++ + + + + + + + + + + + + +
    Table 3. /api/download/docs/{registeredRepoId}
    ParameterDescription

    registeredRepoId

    등록된 레포지토리 정보 고유 ID 값

    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 404 Not Found
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +Content-Type: application/json
    +Content-Length: 105
    +
    +{
    +  "message" : "레포지토리 결과물을 아직 생성중입니다. 잠시만 기다려주세요."
    +}
    +
    +
    +
    +
    +
    +

    멤버가 등록한 레포지토리 리스트 조회

    +
    +

    HTTP Request

    +
    +
    +
    GET /api/member/repos/registered HTTP/1.1
    +Content-Type: application/json;charset=UTF-8
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Accept: application/json
    +Host: localhost:8080
    +
    +
    +
    +
    +

    Request Header

    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    엑세스 토큰

    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 200 OK
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +Content-Type: application/json
    +Content-Length: 692
    +
    +{
    +  "findRegisterRepoResponses" : [ {
    +    "registeredRepoId" : 1,
    +    "repositoryName" : "dododocs",
    +    "branchName" : "main",
    +    "createdAt" : "2024-12-18",
    +    "readmeComplete" : false,
    +    "chatbotComplete" : false,
    +    "docsComplete" : false
    +  }, {
    +    "registeredRepoId" : 2,
    +    "repositoryName" : "moheng",
    +    "branchName" : "develop",
    +    "createdAt" : "2024-12-18",
    +    "readmeComplete" : false,
    +    "chatbotComplete" : false,
    +    "docsComplete" : false
    +  }, {
    +    "registeredRepoId" : 3,
    +    "repositoryName" : "repo-name3",
    +    "branchName" : "main",
    +    "createdAt" : "2024-12-18",
    +    "readmeComplete" : false,
    +    "chatbotComplete" : false,
    +    "docsComplete" : false
    +  } ]
    +}
    +
    +
    +
    +
    +

    Response Field

    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    findRegisterRepoResponses[]

    Array

    등록한 레포지토리 리스트

    findRegisterRepoResponses[].registeredRepoId

    Number

    등록된 레포 고유 ID

    findRegisterRepoResponses[].repositoryName

    String

    레포 이름

    findRegisterRepoResponses[].branchName

    String

    레포 브랜치 명

    findRegisterRepoResponses[].createdAt

    String

    레포 등록날짜

    findRegisterRepoResponses[].readmeComplete

    Boolean

    리드미 생성 완료(준비) 여부

    findRegisterRepoResponses[].chatbotComplete

    Boolean

    챗봇 기능 준비 완료 여부

    findRegisterRepoResponses[].docsComplete

    Boolean

    문서 생성 완료 여부

    +
    +
    +
    +

    등록된 레포지토리 삭제

    +
    +

    HTTP Request

    +
    +
    +
    DELETE /api/register HTTP/1.1
    +Content-Type: application/json;charset=UTF-8
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Accept: application/json
    +Content-Length: 28
    +Host: localhost:8080
    +
    +{
    +  "registeredRepoId" : 1
    +}
    +
    +
    +
    +
    Request Header
    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    엑세스 토큰

    +
    +
    +
    Request Body
    + +++++ + + + + + + + + + + + + + + +
    PathTypeDescription

    registeredRepoId

    Number

    삭제할 레포지토리 고유 ID

    +
    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 204 No Content
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +
    +
    +
    +
    +
    +

    레포에서 특정 파일명을 입력했을 때, 그에 대한 리드미 내용을 제공하는 API

    +
    +

    HTTP Request

    +
    +
    +
    GET /api/download/s3/detail/1?fileName=Controller_Summary.md HTTP/1.1
    +Content-Type: application/json;charset=UTF-8
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Accept: application/json
    +Host: localhost:8080
    +
    +
    +
    +
    Request Header
    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    토큰

    +
    +
    +
    Query Param
    + ++++ + + + + + + + + + + + + +
    ParameterDescription

    fileName

    조회할 파일 이름

    +
    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 200 OK
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +Content-Type: application/json
    +Content-Length: 98
    +
    +{
    +  "fileName" : "Controller_Summary.md",
    +  "fileContents" : "전체 컨트롤러 요약 내용"
    +}
    +
    +
    +
    +
    Response Body
    + +++++ + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    fileName

    String

    요청한 파일 이름

    fileContents

    String

    해당 파일의 내용

    +
    +
    +
    +
    +

    리드미 내용 수정 API

    +
    +

    HTTP Request

    +
    +
    +
    PUT /api/readme?repositoryName=dododocs&fileName=Controller_Summary.md&newContent=new%20contents~~ HTTP/1.1
    +Content-Type: application/json;charset=UTF-8
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Accept: application/json
    +Host: localhost:8080
    +
    +
    +
    +
    Request Param
    + ++++ + + + + + + + + + + + + + + + + + + + + +
    ParameterDescription

    repositoryName

    레포지토리 이름

    fileName

    리드미 파일 이름

    newContent

    새롭게 수정할 리드미 내용

    +
    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 204 No Content
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +
    +
    +
    +
    +
    +
    +
    +

    ⛳️ Chatbot (챗봇)

    +
    +
    +

    챗봇 대화 내역 불러오기

    +
    +

    HTTP Request

    +
    +
    +
    GET /api/chatbot/logs/1 HTTP/1.1
    +Content-Type: application/json;charset=UTF-8
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Accept: application/json
    +Host: localhost:8080
    +
    +
    +
    +
    +

    Request Header

    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    엑세스 토큰

    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 200 OK
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +Content-Type: application/json
    +Content-Length: 213
    +
    +{
    +  "findChatLogResponses" : [ {
    +    "question" : "질문1",
    +    "answer" : "대답1"
    +  }, {
    +    "question" : "질문1",
    +    "answer" : "대답1"
    +  }, {
    +    "question" : "질문1",
    +    "answer" : "대답1"
    +  } ]
    +}
    +
    +
    +
    +
    +
    +

    챗봇에게 질문하기

    +
    +

    HTTP Request

    +
    +
    +
    POST /api/chatbot/question/save/1 HTTP/1.1
    +Content-Type: application/json;charset=UTF-8
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Accept: application/json
    +Content-Length: 45
    +Host: localhost:8080
    +
    +{
    +  "question" : "오늘 밥 언제먹지?"
    +}
    +
    +
    +
    +
    Request Header
    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    엑세스 토큰

    +
    +
    +
    Request Body
    + +++++ + + + + + + + + + + + + + + +
    PathTypeDescription

    question

    String

    챗봇에게 물어볼 질문

    +
    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 200 OK
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +Content-Type: application/json
    +Content-Length: 33
    +
    +{
    +  "answer" : "굶어 인마."
    +}
    +
    +
    +
    +
    +

    Response Body

    + +++++ + + + + + + + + + + + + + + +
    PathTypeDescription

    answer

    String

    챗봇에게 돌아온 대답

    +
    +
    +
    +
    +
    +

    ⛳️ Test (테스트 API)

    +
    +
    +

    AI README 분석 결과 조회 테스트 API

    +
    +

    HTTP Request

    +
    +
    +
    GET /api/analyze/result?repositoryName=Gatsby-Starter-Haon HTTP/1.1
    +Accept: application/json
    +Host: localhost:8080
    +
    +
    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 200 OK
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +Content-Type: application/json
    +Content-Length: 11921
    +
    +{
    +  "summaryFiles" : [ {
    +    "fileName" : "kakao-25_moheng.README.md",
    +    "fileContents" : "    # Project Name\n                moheng\n\n                ## Table of Contents\n                [ \uD83D\uDCDD Overview](#\uD83D\uDCDD-overview)  \n                [ \uD83D\uDCC1 Project Structure](#\uD83D\uDCC1-project-structure)  \n                [ \uD83D\uDE80 Getting Started](#\uD83D\uDE80-getting-started)  \n                [ \uD83D\uDCA1 Motivation](#\uD83D\uDCA1-motivation)  \n                [ \uD83C\uDFAC Demo](#\uD83C\uDFAC-demo)  \n                [ \uD83C\uDF10 Deployment](#\uD83C\uDF10-deployment)  \n                [ \uD83E\uDD1D Contributing](#\uD83E\uDD1D-contributing)  \n                [ ❓ Troubleshooting & FAQ](#❓-troubleshooting-&-faq)  \n                [ \uD83D\uDCC8 Performance](#\uD83D\uDCC8-performance)  \n\n                ## \uD83D\uDCDD Overview\n                이 프로젝트는 여행 계획 및 추천 시스템을 구축하기 위한 것입니다. 사용자는 여행지에 대한 정보를 얻고, 개인화된 추천을 받을 수 있습니다.\n\n                ### Main Purpose\n                - 사용자가 여행 계획을 세우고, 개인의 선호에 맞는 여행지를 추천받을 수 있도록 지원합니다.\n                - 여행지에 대한 정보를 제공하고, 사용자의 클릭 데이터를 기반으로 추천 알고리즘을 통해 맞춤형 여행지를 제안합니다.\n\n                ### Key Features\n                - 사용자 맞춤형 여행지 추천\n                - 여행지 정보 제공\n                - 클릭 데이터를 기반으로 한 추천 알고리즘\n\n                ### Core Technology Stack\n                - Frontend: React, Vite\n                - Backend: Spring Boot\n                - Database: MySQL\n                - Others: Python, FastAPI (AI 모델 서빙)\n\n                ## \uD83D\uDCC1 Project Structure\n                ```\n                moheng\n                ├── \uD83D\uDCC1 ai\n                │   ├── \uD83D\uDCC1 model_serving\n                │   │   ├── \uD83D\uDCC1 application\n                │   │   ├── \uD83D\uDCC1 domain\n                │   │   ├── \uD83D\uDCC1 infra\n                │   │   ├── \uD83D\uDCC1 interface\n                │   │   └── ...\n                │   └── ...\n                ├── \uD83D\uDCC1 frontend\n                │   ├── \uD83D\uDCC1 src\n                │   │   ├── \uD83D\uDCC1 api\n                │   │   ├── \uD83D\uDCC1 components\n                │   │   └── ...\n                │   └── ...\n                ├── \uD83D\uDCC1 moheng\n                │   ├── \uD83D\uDCC1 auth\n                │   ├── \uD83D\uDCC1 planner\n                │   ├── \uD83D\uDCC1 trip\n                │   ├── \uD83D\uDCC1 member\n                │   └── ...\n                └── ...\n                ```\n\n                ## \uD83D\uDE80 Getting Started\n\n                ### Prerequisites\n\n                - 지원 운영 체제\n                  * Windows, macOS, Linux\n                - 필수 소프트웨어\n                  * Node.js (프론트엔드)\n                  * Java (백엔드)\n                  * Python 3.11 (AI)\n                - 버전 요구 사항\n                  * Node.js: 최신 안정 버전\n                  * Java: OpenJDK 22\n                  * Python: 3.11.x\n                - 패키지 관리자\n                  * npm (프론트엔드)\n                  * Poetry (AI)\n                - 시스템 종속성\n                  * Docker\n\n                ### Installation\n\n                - Docker를 사용하여 환경을 설정할 수 있습니다.\n\n                ```bash\n                # 리포지토리 클론\n                git clone https://github.com/kakao-25/moheng.git\n                cd moheng-develop\n\n                # Docker 설정을 위해 Docker가 시스템에 설치되고 실행 중인지 확인하십시오.\n\n                # Docker 컨테이너 빌드 및 실행\n                docker-compose up --build\n\n                # 각 구성 요소를 별도로 빌드하고 실행하려면 아래 단계를 따르십시오:\n\n                # 프론트엔드 설정\n                cd frontend\n                docker build -t moheng-frontend -f Dockerfile.front .\n                docker run -p 3000:3000 moheng-frontend\n\n                # 백엔드 설정\n                cd backend\n                docker build -t moheng-backend -f Dockerfile.prod .\n                docker run -p 8080:8080 moheng-backend\n\n                # AI/서비스 설정\n                cd ai\n                docker build -t moheng-ai -f Dockerfile .\n                docker run -p 8000:8000 moheng-ai\n\n                # Nginx 설정\n                cd nginx\n                docker build -t moheng-nginx -f Dockerfile.prod .\n                docker run -p 80:80 moheng-nginx\n\n                # 환경 구성\n                # 각 구성 요소에 필요한 환경 변수가 설정되어 있는지 확인하십시오.\n                ```\n\n                ### Usage\n\n                ```bash\n                # 실행 방법\n                # Docker 컨테이너 설정 후, 다음 URL을 통해 서비스를 액세스하십시오:\n\n                # 프론트엔드\n                http://localhost:3000\n\n                # 백엔드\n                http://localhost:8080\n\n                # AI 서비스\n                http://localhost:8000\n\n                # Nginx (리버스 프록시 역할)\n                http://localhost\n\n\n                ## \uD83D\uDCA1 Motivation\n                이 프로젝트는 여행 계획을 세우는 데 있어 사용자에게 더 나은 경험을 제공하기 위해 시작되었습니다. 개인의 선호를 반영한 추천 시스템을 통해 사용자가 더 쉽게 여행지를 선택할 수 있도록 돕고자 합니다.\n\n                ## \uD83C\uDFAC Demo\n                ![Demo Video or Screenshot](path/to/demo.mp4)\n\n                ## \uD83C\uDF10 Deployment\n                - AWS, Heroku와 같은 클라우드 플랫폼에 배포 가능\n                - 배포 단계에 따라 환경 설정이 필요합니다.\n\n                ## \uD83E\uDD1D Contributing\n                - 기여 방법: 이슈를 생성하거나 Pull Request를 통해 기여할 수 있습니다.\n                - 코드 표준: Java, Python, JavaScript의 일반적인 코딩 표준을 따릅니다.\n                - Pull Request 프로세스: 변경 사항을 설명하는 메시지와 함께 Pull Request를 제출합니다.\n                - 행동 강령: 모든 기여자는 존중과 배려의 태도로 참여해야 합니다.\n\n                ## ❓ Troubleshooting & FAQ\n                - 자주 발생하는 문제 및 해결 방법을 문서화합니다.\n                - FAQ 섹션을 통해 사용자 질문에 대한 답변을 제공합니다.\n\n                ## \uD83D\uDCC8 Performance\n                - 성능 벤치마크 및 최적화 기법을 문서화합니다.\n                - 시스템의 확장성 고려 사항을 포함합니다.\n"
    +  } ],
    +  "regularFiles" : [ {
    +    "fileName" : "AuthController.md",
    +    "fileContents" : "    아래는 제공된 Java 코드에 대한 아키텍처 문서입니다. 이 문서는 시스템의 구조와 각 컴포넌트의 역할을 설명합니다.\n\n                # 시스템 아키텍처 문서\n\n                ## 전체 구조\n                ```mermaid\n                graph TD\n                    A[Client] --> B[AuthController]\n                    B --> C[AuthService]\n                    C --> D[TokenManager]\n                    C --> E[MemberService]\n                    D --> F[MemberToken]\n                    E --> G[Member]\n                ```\n\n                ## 시스템 흐름\n                ```mermaid\n                sequenceDiagram\n                    Client->>AuthController: Request for URI\n                    AuthController->>AuthService: generateUri()\n                    AuthService-->>AuthController: Return URI\n                    AuthController->>Client: Send URI\n\n                    Client->>AuthController: Login Request\n                    AuthController->>AuthService: generateTokenWithCode()\n                    AuthService-->>AuthController: Return MemberToken\n                    AuthController->>Client: Send Access Token\n\n                    Client->>AuthController: Extend Login Request\n                    AuthController->>AuthService: generateRenewalAccessToken()\n                    AuthService-->>AuthController: Return RenewalAccessTokenResponse\n                    AuthController->>Client: Send New Access Token\n\n                    Client->>AuthController: Logout Request\n                    AuthController->>AuthService: removeRefreshToken()\n                    AuthService-->>AuthController: Refresh Token Removed\n                    AuthController->>Client: No Content Response\n                ```\n\n                ## 주요 컴포넌트 설명\n\n                ### AuthController\n                - **역할과 책임**: 클라이언트의 요청을 처리하고, 서비스 계층과의 상호작용을 통해 인증 관련 작업을 수행합니다.\n                - **주요 메서드**:\n                  - `generateUri()`: OAuth 제공자로부터 인증 URI를 생성합니다.\n                  - `login()`: OAuth 인증 코드를 사용하여 로그인하고, 액세스 토큰을 생성합니다.\n                  - `extendLogin()`: 기존의 리프레시 토큰을 사용하여 새로운 액세스 토큰을 생성합니다.\n                  - `logout()`: 리프레시 토큰을 제거하여 로그아웃을 처리합니다.\n\n                ### AuthService\n                - **역할과 책임**: 인증 관련 비즈니스 로직을 처리합니다. OAuth 제공자와의 상호작용, 멤버 생성 및 토큰 관리를 담당합니다.\n                - **주요 메서드**:\n                  - `generateTokenWithCode()`: OAuth 인증 코드를 사용하여 멤버 토큰을 생성합니다.\n                  - `generateUri()`: OAuth 제공자로부터 URI를 생성합니다.\n                  - `generateRenewalAccessToken()`: 리프레시 토큰을 사용하여 새로운 액세스 토큰을 생성합니다.\n                  - `removeRefreshToken()`: 로그아웃 시 리프레시 토큰을 제거합니다.\n                  - `extractMemberId()`: 액세스 토큰에서 멤버 ID를 추출합니다.\n\n                ### TokenManager\n                - **역할과 책임**: 토큰 생성 및 검증을 담당합니다. 액세스 토큰과 리프레시 토큰의 생성 및 관리 기능을 제공합니다.\n\n                ### MemberService\n                - **역할과 책임**: 멤버 관련 데이터의 CRUD 작업을 처리합니다. 멤버의 존재 여부를 확인하고, 새로운 멤버를 생성합니다.\n\n                ### MemberToken\n                - **역할과 책임**: 액세스 토큰과 리프레시 토큰을 포함하는 데이터 구조입니다. 인증 과정에서 생성된 토큰 정보를 저장합니다.\n\n                ## 주의사항\n                1. 각 컴포넌트의 역할과 책임을 명확히 이해하고, 필요 시 추가적인 설명을 덧붙이세요.\n                2. 시스템의 흐름을 시퀀스 다이어그램으로 표현하여 클라이언트와 서버 간의 상호작용을 명확히 하세요.\n                3. 각 메서드의 기능과 사용 예를 문서화하여 개발자들이 쉽게 이해할 수 있도록 하세요.\n"
    +  } ]
    +}
    +
    +
    +
    +
    +
    +

    리드미 수정 테스트 API

    +
    +

    HTTP Request

    +
    +
    +
    PUT /api/test/readme/update?repositoryName=dododocs&fileName=Controller_Summary.md&newContent=new%20contents~~ HTTP/1.1
    +Content-Type: application/json;charset=UTF-8
    +Authorization: Bearer aaaaaa.bbbbbb.cccccc
    +Accept: application/json
    +Host: localhost:8080
    +
    +
    +
    +
    Request Param
    + ++++ + + + + + + + + + + + + + + + + + + + + +
    ParameterDescription

    repositoryName

    레포지토리 이름

    fileName

    리드미 파일 이름

    newContent

    새롭게 수정할 리드미 내용

    +
    +
    +
    +

    HTTP Response

    +
    +
    +
    HTTP/1.1 204 No Content
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +
    +
    +
    +
    diff --git a/src/test/java/dododocs/dododocs/analyze/AnalyzeControllerTest.java b/src/test/java/dododocs/dododocs/analyze/AnalyzeControllerTest.java new file mode 100644 index 0000000..e0ccbbc --- /dev/null +++ b/src/test/java/dododocs/dododocs/analyze/AnalyzeControllerTest.java @@ -0,0 +1,140 @@ +package dododocs.dododocs.analyze; + +import dododocs.dododocs.analyze.dto.FindGitRepoContentRequest; +import dododocs.dododocs.analyze.dto.UploadGitRepoContentToS3Request; +import dododocs.dododocs.analyze.exception.MaxSizeRepoRegiserException; +import dododocs.dododocs.analyze.exception.NoExistGitRepoException; +import dododocs.dododocs.config.ControllerTestConfig; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class AnalyzeControllerTest extends ControllerTestConfig { + + @DisplayName("AI 레포지토리 분석 결과를 요청하고 상태코드 200을 리턴한다.") + @Test + void AI_레포지토리_분석_결과를_요청하고_상태코드_200을_리턴한다() throws Exception { + // given + given(jwtTokenCreator.extractMemberId(anyString())).willReturn(1L); + doNothing().when(analyzeService).uploadGithubRepoToS3(any(), anyLong()); + + // when, then + mockMvc.perform(post("/api/upload/s3") + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new UploadGitRepoContentToS3Request("Gatsby-Starter-Haon", "main", true, true + )))) + .andDo(print()) + .andDo(document("analyze/upload/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestFields( + fieldWithPath("repositoryName").type(JsonFieldType.STRING).description("분석 할 레포지토리 이름 (ex. Gatsby-Starter-Haon)"), + fieldWithPath("branchName").type(JsonFieldType.STRING).description("브랜치 명 (ex. main)"), + fieldWithPath("korean").type(JsonFieldType.BOOLEAN).description("한국어 번역 여부"), + fieldWithPath("includeTest").type(JsonFieldType.BOOLEAN).description("테스트 포함 여부") + ))) + .andExpect(status().isOk()); + } + + @DisplayName("레포지토리 등록시 3개 이상 등록한 상태라면 예외가 발생한다.") + @Test + void registerRepoMaxUpSizeExceptionTest() throws Exception { + // given + given(jwtTokenCreator.extractMemberId(anyString())).willReturn(1L); + doThrow(new MaxSizeRepoRegiserException("최대 3개의 레포지토리를 등록 가능합니다.")) + .when(analyzeService).uploadGithubRepoToS3(any(), anyLong()); + + // when, then + mockMvc.perform(post("/api/upload/s3") + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new UploadGitRepoContentToS3Request("Gatsby-Starter-Haon", "main", true, true)))) + .andDo(print()) + .andDo(document("analyze/upload/fail/max/size", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName("Authorization").description("엑세스 토큰") + ), + requestFields( + fieldWithPath("repositoryName").type(JsonFieldType.STRING).description("분석 할 레포지토리 이름 (ex. Gatsby-Starter-Haon)"), + fieldWithPath("branchName").type(JsonFieldType.STRING).description("브랜치 명 (ex. main)"), + fieldWithPath("korean").type(JsonFieldType.BOOLEAN).description("한국어 번역 여부"), + fieldWithPath("includeTest").type(JsonFieldType.BOOLEAN).description("테스트 포함 여부") + ))) + .andExpect(status().isBadRequest()); + } + + @DisplayName("존재하지 않는 레포지토리 또는 브랜치를 입력시 상태코드 404를 리턴한다.") + @Test + void registerNotFoundGitRepoException() throws Exception { + // given + given(jwtTokenCreator.extractMemberId(anyString())).willReturn(1L); + doThrow(new NoExistGitRepoException("존재하지 않는 레포지토리 또는 브랜치입니다.")) + .when(analyzeService).uploadGithubRepoToS3(any(), anyLong()); + + // when, then + mockMvc.perform(post("/api/upload/s3") + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new UploadGitRepoContentToS3Request("Invalid-Git-Repo-Name", "main222", true, true)))) + .andDo(print()) + .andDo(document("analyze/upload/fail/noExistRepoInfo", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName("Authorization").description("엑세스 토큰") + ), + requestFields( + fieldWithPath("repositoryName").type(JsonFieldType.STRING).description("분석 할 레포지토리 이름 (ex. Gatsby-Starter-Haon)"), + fieldWithPath("branchName").type(JsonFieldType.STRING).description("브랜치 명 (ex. main)"), + fieldWithPath("korean").type(JsonFieldType.BOOLEAN).description("한국어 번역 여부"), + fieldWithPath("includeTest").type(JsonFieldType.BOOLEAN).description("테스트 포함 여부") + ))) + .andExpect(status().isNotFound()); + } + + @DisplayName("레포지토리의 모든 코드를 읽어오고 상태코드 200을 리턴한다.") + @Test + void 레포지토리의_모든_코드를_읽어오고_상태코드_200을_리턴한다() throws Exception{ + /* // given + given(jwtTokenCreator.extractMemberId(anyString())).willReturn(1L); + doNothing().when(analyzeService).uploadGithubRepoToS3(any(), anyLong()); + + // when, then + mockMvc.perform(get("/api/repo/contents") + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new FindGitRepoContentRequest("Gatsby-Starter-Haon", "main")))) + .andDo(print()) + .andDo(document("find/repo/content/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestFields( + fieldWithPath("repositoryName").type(JsonFieldType.STRING).description("코드를 가져올 레포지토리 이름 (ex. Gatsby-Starter-Haon)"), + fieldWithPath("branchName").type(JsonFieldType.STRING).description("브랜치 명 (ex. main)") + ))) + .andExpect(status().isOk()); */ + } +} diff --git a/src/test/java/dododocs/dododocs/analyze/DownloadControllerTest.java b/src/test/java/dododocs/dododocs/analyze/DownloadControllerTest.java new file mode 100644 index 0000000..24fe557 --- /dev/null +++ b/src/test/java/dododocs/dododocs/analyze/DownloadControllerTest.java @@ -0,0 +1,204 @@ +package dododocs.dododocs.analyze; + +import dododocs.dododocs.analyze.dto.DownloadAiAnalyzeRequest; +import dododocs.dododocs.analyze.dto.DownloadAiAnalyzeResponse; +import dododocs.dododocs.analyze.dto.DownloadReadmeAnalyzeResponse; +import dododocs.dododocs.analyze.exception.NoExistRepoAnalyzeException; +import dododocs.dododocs.auth.dto.LoginRequest; +import dododocs.dododocs.config.ControllerTestConfig; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; + +import java.io.FileNotFoundException; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class DownloadControllerTest extends ControllerTestConfig { + + @DisplayName("Docs 를 다운로드 받고 상태코드 200을 리턴한다.") + @Test + void AI_문서화_결과를_다운로드_받고_상태코드_200을_리턴한다() throws Exception { + // given + given(jwtTokenCreator.extractMemberId(anyString())).willReturn(1L); + given(downloadFromS3Service.downloadAndProcessZipDocsInfo(anyLong())) + .willReturn(new DownloadAiAnalyzeResponse( + List.of(new DownloadAiAnalyzeResponse.FileDetail("Controller_Summary.md", "전체 컨트롤러 요약 내용"), + new DownloadAiAnalyzeResponse.FileDetail("Service_Summary.md", "전체 서비스 요약 내용")), + List.of(new DownloadAiAnalyzeResponse.FileDetail("AuthController.md", "설명1"), + new DownloadAiAnalyzeResponse.FileDetail("AuthService.md", "설명2"), + new DownloadAiAnalyzeResponse.FileDetail("TravelController.md", "설명3")) + )); + + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/download/docs/{registeredRepoId}", 1L) + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andDo(document("analyze/download/docs/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName("Authorization").description("엑세스 토큰") + ), + pathParameters( + parameterWithName("registeredRepoId").description("등록된 레포지토리 정보 고유 ID 값") + ), + responseFields( + fieldWithPath("summaryFiles[]").type(JsonFieldType.ARRAY).description("요약 파일 목록"), + fieldWithPath("summaryFiles[].fileName").type(JsonFieldType.STRING).description("요약 파일 이름"), + fieldWithPath("summaryFiles[].fileContents").type(JsonFieldType.STRING).description("요약 파일 내용"), + fieldWithPath("regularFiles[]").type(JsonFieldType.ARRAY).description("일반 파일 목록"), + fieldWithPath("regularFiles[].fileName").type(JsonFieldType.STRING).description("일반 파일 이름"), + fieldWithPath("regularFiles[].fileContents").type(JsonFieldType.STRING).description("일반 파일 내용") + ) + )) + .andExpect(status().isOk()); + + } + + @DisplayName("Readme 를 다운로드 받고 상태코드 200을 리턴한다.") + @Test + void AI_Readme_결과를_다운로드_받고_상태코드_200을_리턴한다() throws Exception { + // given + given(jwtTokenCreator.extractMemberId(anyString())).willReturn(1L); + given(downloadFromS3Service.downloadAndProcessZipReadmeInfo(anyLong())) + .willReturn(new DownloadReadmeAnalyzeResponse( + "AI 분석 결과 리드미 내용물" + )); + + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/download/readme/{registeredRepoId}", 1L) + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andDo(document("analyze/download/readme/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName("Authorization").description("엑세스 토큰") + ), + pathParameters( + parameterWithName("registeredRepoId").description("등록된 레포지토리 정보 고유 ID 값") + ), + responseFields( + fieldWithPath("contents").type(JsonFieldType.STRING).description("리드미 내용") + ) + )) + .andExpect(status().isOk()); + + } + + @DisplayName("아직 AI 분석 결과가 완료되지 않았다면 상태코드 404를 리턴한다.") + @Test + void 아직_AI_분석_결과가_완료되지_않았다면_상태코드_404을_리턴한다() throws Exception { + // given + given(authService.extractMemberId(anyString())).willReturn(1L); + doThrow(new NoExistRepoAnalyzeException("레포지토리 결과물을 아직 생성중입니다. 잠시만 기다려주세요.")) + .when(downloadFromS3Service).downloadAndProcessZipDocsInfo(anyLong()); + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/download/docs/{registeredRepoId}", 1L) // registeredRepoId 전달 + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andDo(document("analyze/download/docs/fail", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("registeredRepoId").description("등록된 레포지토리 정보 고유 ID 값") + ) + )) + .andExpect(status().isNotFound()); + } + + @DisplayName("레포에서 특정 파일명 입력했을 때, 그에 대한 리드미 내용을 제공한다.") + @Test + void getFileContentByFileName_ValidFile_ReturnsContent() throws Exception { + // given + given(authService.extractMemberId(anyString())).willReturn(1L); + given(downloadFromS3Service.downloadAndProcessZipReadmeInfoByRepoName(anyLong())).willReturn( + new DownloadAiAnalyzeResponse( + List.of(new DownloadAiAnalyzeResponse.FileDetail("Controller_Summary.md", "전체 컨트롤러 요약 내용")), + List.of(new DownloadAiAnalyzeResponse.FileDetail("AuthService.md", "설명2")) + ) + ); + + // when, then + mockMvc.perform(get("/api/download/s3/detail/{registeredRepoId}", 1) + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .queryParam("fileName", "Controller_Summary.md") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andDo(document("download/repo/file/detail/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName("Authorization").description("토큰") + ), + queryParameters( + parameterWithName("fileName").description("조회할 파일 이름") + ), + responseFields( + fieldWithPath("fileName").description("요청한 파일 이름"), + fieldWithPath("fileContents").description("해당 파일의 내용") + ) + )) + .andExpect(status().isOk()); + } + + @DisplayName("리드미 내용을 수정하고 성공 메시지를 반환한다.") + @Test + void updateFileContent_Success_ReturnsOk() throws Exception { + // given + doNothing().when(downloadFromS3Service) + .updateFileContent(anyString(), anyString(), anyString()); + + // when, then + mockMvc.perform(put("/api/readme") + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .queryParam("repositoryName", "dododocs") + .queryParam("fileName", "Controller_Summary.md") + .queryParam("newContent", "new contents~~") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andDo(document("readme/update/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName("Authorization").description("토큰") + ), + queryParameters( + parameterWithName("repositoryName").description("레포지토리 이름"), + parameterWithName("fileName").description("리드미 파일 이름"), + parameterWithName("newContent").description("새롭게 수정할 리드미 내용") + ) + )) + .andExpect(status().isNoContent()); + } +} diff --git a/src/test/java/dododocs/dododocs/analyze/RepoRegisterControllerTest.java b/src/test/java/dododocs/dododocs/analyze/RepoRegisterControllerTest.java new file mode 100644 index 0000000..dd4520a --- /dev/null +++ b/src/test/java/dododocs/dododocs/analyze/RepoRegisterControllerTest.java @@ -0,0 +1,59 @@ +package dododocs.dododocs.analyze; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import dododocs.dododocs.analyze.dto.DeleteRepoRegisterRequest; +import dododocs.dododocs.analyze.dto.DownloadAiAnalyzeRequest; +import dododocs.dododocs.analyze.dto.DownloadAiAnalyzeResponse; +import dododocs.dododocs.analyze.dto.FindRepoRegisterResponses; +import dododocs.dododocs.config.ControllerTestConfig; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; + +import java.util.List; +import java.util.Map; + +public class RepoRegisterControllerTest extends ControllerTestConfig { + + @DisplayName("등록된 레포를 삭제하고 상태코드 204를 리턴한다.") + @Test + void deleteRegisteredRepoControllerTest() throws Exception { + // given + given(jwtTokenCreator.extractMemberId(anyString())).willReturn(1L); + doNothing().when(repoRegisterService).removeRegisteredRepos(anyLong()); + + // when, then + mockMvc.perform(delete("/api/register") + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new DeleteRepoRegisterRequest(1L)))) + .andDo(print()) + .andDo(document("register/delete/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName("Authorization").description("엑세스 토큰") + ), + requestFields( + fieldWithPath("registeredRepoId").description("삭제할 레포지토리 고유 ID") + ) + )) + .andExpect(status().isNoContent()); + } +} diff --git a/src/test/java/dododocs/dododocs/auth/AuthControllerTest.java b/src/test/java/dododocs/dododocs/auth/AuthControllerTest.java index d8adb75..70aa209 100644 --- a/src/test/java/dododocs/dododocs/auth/AuthControllerTest.java +++ b/src/test/java/dododocs/dododocs/auth/AuthControllerTest.java @@ -2,6 +2,7 @@ import dododocs.dododocs.auth.dto.LoginRequest; +import dododocs.dododocs.auth.exception.InvalidTokenException; import dododocs.dododocs.config.ControllerTestConfig; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -13,6 +14,8 @@ import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; @@ -64,4 +67,25 @@ void generateSocialLoginLink() throws Exception { )) .andExpect(status().isCreated()); } + + @DisplayName("만료되었거나 잘못 변형된 리프레시 토큰으로 새로운 엑세스 토큰을 재발급하려 하면 상태코드 401을 리턴한다.") + @Test + void 만료되었거나_잘못_변형된_리프레시_토큰으로_새로운_엑세스_토큰을_발급하려_하면_상태코드_401을_리턴한다() throws Exception { + // given + given(authService.generateUri()).willThrow(new InvalidTokenException("변조되었거나 만료된 토큰 입니다.")); + + // when, then + mockMvc.perform(get("/api/auth/link") + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + ) + .andDo(print()) + .andDo(document("auth/logout/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName("Authorization").description("엑세스 토큰") + ) + )) + .andExpect(status().isUnauthorized()); + } } diff --git a/src/test/java/dododocs/dododocs/chatbot/ChatbotControllerTest.java b/src/test/java/dododocs/dododocs/chatbot/ChatbotControllerTest.java new file mode 100644 index 0000000..d1aa3d2 --- /dev/null +++ b/src/test/java/dododocs/dododocs/chatbot/ChatbotControllerTest.java @@ -0,0 +1,93 @@ +package dododocs.dododocs.chatbot; + +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import dododocs.dododocs.analyze.dto.UploadGitRepoContentToS3Request; +import dododocs.dododocs.chatbot.domain.ChatLog; +import dododocs.dododocs.chatbot.dto.ExternalQuestToChatbotResponse; +import dododocs.dododocs.chatbot.dto.FindChatLogResponses; +import dododocs.dododocs.chatbot.dto.QuestToChatbotRequest; +import dododocs.dododocs.config.ControllerTestConfig; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; + +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class ChatbotControllerTest extends ControllerTestConfig { + + @DisplayName("챗봇 대화 내역을 불러온다.") + @Test + void findChatbotHistory() throws Exception { + // given + given(jwtTokenCreator.extractMemberId(anyString())).willReturn(1L); + given(chatbotService.findChatbotHistory(anyLong(), any())) + .willReturn(new FindChatLogResponses(List.of( + new ChatLog("질문1", "대답1", new RepoAnalyze(1L, "repo-name")), + new ChatLog("질문1", "대답1", new RepoAnalyze(1L, "repo-name")), + new ChatLog("질문1", "대답1", new RepoAnalyze(1L, "repo-name")) + ))); + + // when, then + mockMvc.perform(get("/api/chatbot/logs/{registeredRepoId}", 1L) + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andDo(document("chatbot/find/logs/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName("Authorization").description("엑세스 토큰") + ) + )) + .andExpect(status().isOk()); + } + + /* @DisplayName("챗봇에게 질문하기") + @Test + void askToChatbot() throws Exception { + // given + given(jwtTokenCreator.extractMemberId(anyString())).willReturn(1L); + given(chatbotService.questionToChatbotAndSaveLogs(anyLong(), anyString())) + .willReturn(new ExternalQuestToChatbotResponse("굶어 인마.")); + + // when, then + mockMvc.perform(post("/api/chatbot/question/save/{registeredRepoId}", 1L) + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new QuestToChatbotRequest("오늘 밥 언제먹지?") + ))) + .andDo(print()) + .andDo(document("chatbot/ask/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName("Authorization").description("엑세스 토큰") + ), + requestFields( + fieldWithPath("question").description("챗봇에게 물어볼 질문") + ), + responseFields( + fieldWithPath("answer").description("챗봇에게 돌아온 대답") + ) + )) + .andExpect(status().isOk()); + } */ +} diff --git a/src/test/java/dododocs/dododocs/config/ControllerTestConfig.java b/src/test/java/dododocs/dododocs/config/ControllerTestConfig.java index 84fe421..46a30e9 100644 --- a/src/test/java/dododocs/dododocs/config/ControllerTestConfig.java +++ b/src/test/java/dododocs/dododocs/config/ControllerTestConfig.java @@ -2,16 +2,28 @@ import com.fasterxml.jackson.databind.ObjectMapper; import dododocs.dododocs.analyze.application.AnalyzeService; +import dododocs.dododocs.analyze.application.DownloadFromS3Service; +import dododocs.dododocs.analyze.application.RepoRegisterService; +import dododocs.dododocs.analyze.domain.repository.RepoAnalyzeRepository; +import dododocs.dododocs.analyze.presentation.AnalyzeController; +import dododocs.dododocs.analyze.presentation.RepoRegisterController; +import dododocs.dododocs.analyze.presentation.S3DownloadController; import dododocs.dododocs.auth.application.AuthService; import dododocs.dododocs.auth.domain.GithubOAuthUriProvider; import dododocs.dododocs.auth.domain.JwtTokenCreator; import dododocs.dododocs.auth.domain.JwtTokenProvider; +import dododocs.dododocs.auth.domain.repository.MemberRepository; import dododocs.dododocs.auth.infrastructure.GithubOAuthClient; import dododocs.dododocs.auth.presentation.AuthController; import dododocs.dododocs.auth.presentation.authentication.AuthenticationBearerExtractor; +import dododocs.dododocs.chatbot.application.ChatbotService; +import dododocs.dododocs.chatbot.domain.repository.ChatLogRepository; +import dododocs.dododocs.chatbot.presentation.ChatbotController; import dododocs.dododocs.global.config.S3Config; import dododocs.dododocs.member.application.MemberService; import dododocs.dododocs.member.presentation.MemberController; +import dododocs.dododocs.test.ApiTestController; +import dododocs.dododocs.test.infrastructure.ExternalAiTestClient; import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; @@ -20,11 +32,17 @@ import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.reactive.function.client.WebClient; @AutoConfigureRestDocs @WebMvcTest({ AuthController.class, MemberController.class, + S3DownloadController.class, + AnalyzeController.class, + ApiTestController.class, + RepoRegisterController.class, + ChatbotController.class, }) @Import(TestConfig.class) @ActiveProfiles("test") @@ -52,4 +70,31 @@ public abstract class ControllerTestConfig { @MockBean protected GithubOAuthUriProvider githubOAuthUriProvider; + + @MockBean + protected AnalyzeService analyzeService; + + @MockBean + protected DownloadFromS3Service downloadFromS3Service; + + @MockBean + protected ExternalAiTestClient externalAiTestClient; + + @MockBean + protected MemberRepository memberRepository; + + @MockBean + protected RepoRegisterService repoRegisterService; + + @MockBean + protected ChatbotService chatbotService; + + @MockBean + protected ChatLogRepository chatLogRepository; + + @MockBean + protected RepoAnalyzeRepository repoAnalyzeRepository; + + @MockBean + protected WebClient webClient; } diff --git a/src/test/java/dododocs/dododocs/member/MemberControllerTest.java b/src/test/java/dododocs/dododocs/member/MemberControllerTest.java index dfc9360..61784cb 100644 --- a/src/test/java/dododocs/dododocs/member/MemberControllerTest.java +++ b/src/test/java/dododocs/dododocs/member/MemberControllerTest.java @@ -19,12 +19,19 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import dododocs.dododocs.analyze.domain.RepoAnalyze; +import dododocs.dododocs.analyze.dto.DownloadAiAnalyzeRequest; +import dododocs.dododocs.chatbot.dto.FindMemberInfoResponse; import dododocs.dododocs.config.ControllerTestConfig; +import dododocs.dododocs.member.domain.Member; +import dododocs.dododocs.member.dto.FindRegisterMemberRepoResponses; +import dododocs.dododocs.member.dto.FindRegisterRepoResponse; import dododocs.dododocs.member.dto.FindRepoNameListResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import java.util.List; @@ -39,7 +46,7 @@ void findMemberGitRepoNameList() throws Exception { .willReturn(new FindRepoNameListResponse(List.of("repo-name1", "repo-name2", "repo-name3"))); // when, then - mockMvc.perform(get("/api/member/repos") + mockMvc.perform(get("/api/member/repos/all") .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)) @@ -56,4 +63,68 @@ void findMemberGitRepoNameList() throws Exception { )) .andExpect(status().isOk()); } + + @DisplayName("멤버가 등록한 레포지토리 리스트를 조회하고 상태코드 200을 리턴한다.") + @Test + void findMemberRegisteredRepoList() throws Exception { + // given + given(authService.extractMemberId(anyString())).willReturn(1L); + given(memberService.findRegisterMemberRepoResponses(anyLong())) + .willReturn(new FindRegisterMemberRepoResponses(List.of( + new FindRegisterRepoResponse(new RepoAnalyze(1L, "dododocs", "main", "key1", "key1", "https://dododocs.github.com",new Member(""))), + new FindRegisterRepoResponse(new RepoAnalyze(2L, "moheng", "develop", "key2", "key3", "https://moheng.github.com",new Member(""))), + new FindRegisterRepoResponse(new RepoAnalyze(3L, "repo-name3", "main", "key2", "key3", "https://repo-name3.github.com",new Member(""))) + ))); + + // when, then + mockMvc.perform(get("/api/member/repos/registered") + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andDo(document("repos/registered/find/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName("Authorization").description("엑세스 토큰") + ), + responseFields( + fieldWithPath("findRegisterRepoResponses[]").description("등록한 레포지토리 리스트"), + fieldWithPath("findRegisterRepoResponses[].registeredRepoId").description("등록된 레포 고유 ID"), + fieldWithPath("findRegisterRepoResponses[].repositoryName").description("레포 이름"), + fieldWithPath("findRegisterRepoResponses[].branchName").description("레포 브랜치 명"), + fieldWithPath("findRegisterRepoResponses[].createdAt").description("레포 등록날짜"), + fieldWithPath("findRegisterRepoResponses[].readmeComplete").description("리드미 생성 완료(준비) 여부"), + fieldWithPath("findRegisterRepoResponses[].chatbotComplete").description("챗봇 기능 준비 완료 여부"), + fieldWithPath("findRegisterRepoResponses[].docsComplete").description("문서 생성 완료 여부") + ) + )) + .andExpect(status().isOk()); + } + + @DisplayName("멤버의 기본 프로필 정보를 조회하고 상태코드 200을 리턴한다.") + @Test + void findMemberInfoTest() throws Exception { + given(authService.extractMemberId(anyString())).willReturn(1L); + given(memberService.findMemberInfo(anyLong())) + .willReturn(new FindMemberInfoResponse("devhaon")); + + // when, then + mockMvc.perform(get("/api/member/info") + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andDo(document("member/profile/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName("Authorization").description("엑세스 토큰") + ), + responseFields( + fieldWithPath("nickname").description("깃허브 닉네임") + ) + )) + .andExpect(status().isOk()); + } } diff --git a/src/test/java/dododocs/dododocs/test/ApiTestControllerTest.java b/src/test/java/dododocs/dododocs/test/ApiTestControllerTest.java new file mode 100644 index 0000000..4c162e7 --- /dev/null +++ b/src/test/java/dododocs/dododocs/test/ApiTestControllerTest.java @@ -0,0 +1,89 @@ +package dododocs.dododocs.test; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import dododocs.dododocs.analyze.dto.DownloadAiAnalyzeRequest; +import dododocs.dododocs.analyze.dto.UploadGitRepoContentToS3Request; +import dododocs.dododocs.config.ControllerTestConfig; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.preprocess.ContentModifyingOperationPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationPreprocessor; +import org.springframework.restdocs.payload.JsonFieldType; + +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public class ApiTestControllerTest extends ControllerTestConfig { + + @DisplayName("analyzeResultTest API: 문서화 결과를 정상적으로 반환하고 상태코드 200을 리턴한다.") + @Test + void analyzeResultTest_정상_응답_200() throws Exception { + mockMvc.perform(get("/api/analyze/result") + .param("repositoryName", "Gatsby-Starter-Haon") + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andDo(document("test/analyze/result/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("repositoryName").description("레포명") + ), + responseFields( + fieldWithPath("summaryFiles").type(JsonFieldType.ARRAY).description("요약 파일 목록"), + fieldWithPath("summaryFiles[].*").type(JsonFieldType.STRING).description("요약 파일의 이름과 내용"), + fieldWithPath("regularFiles").type(JsonFieldType.ARRAY).description("일반 파일 목록"), + fieldWithPath("regularFiles[].*").type(JsonFieldType.STRING).description("일반 파일의 이름과 내용") + ) + )); + + } + + @DisplayName("테스트 API - 리드미 내용을 수정하고 성공 메시지를 반환한다.") + @Test + void updateFileContent_Success_ReturnsOk() throws Exception { + // given, when, then + mockMvc.perform(put("/api/test/readme/update") + .header("Authorization", "Bearer aaaaaa.bbbbbb.cccccc") + .queryParam("repositoryName", "dododocs") + .queryParam("fileName", "Controller_Summary.md") + .queryParam("newContent", "new contents~~") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andDo(document("test/readme/update/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName("Authorization").description("토큰") + ), + queryParameters( + parameterWithName("repositoryName").description("레포지토리 이름"), + parameterWithName("fileName").description("리드미 파일 이름"), + parameterWithName("newContent").description("새롭게 수정할 리드미 내용") + ) + )) + .andExpect(status().isNoContent()); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 32d878e..d42e0ce 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -50,4 +50,9 @@ cloud: accessKey: 123 secretKey: 123 +ai: + basic_url: 123 + + +