diff --git a/.env b/.env deleted file mode 100644 index e69de29..0000000 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 7d63d23..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,16 +0,0 @@ - -## 💫 관련 이슈 - - - -## 📌 내용 요약 - - - - -## 🧑🏻‍💻 작업 내용 - - - -## ✅ 추가 사항 - \ No newline at end of file diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml new file mode 100644 index 0000000..c0f2097 --- /dev/null +++ b/.github/workflows/build_and_deploy.yml @@ -0,0 +1,58 @@ +name: Deploy to DockerHub & EC2 + +on: + push: + branches: [ main, develop ] + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 + with: + cache-read-only: false + + - name: Grant Execute Permission For Gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: | + set -e + ./gradlew clean build -Dspring.profiles.active=local + + - name: Build Docker image + run: | + docker build \ + --build-arg SPRING_PROFILES_ACTIVE=prob \ + -f Dockerfile \ + -t ${{ vars.DOCKERHUB_USERNAME}}/erica-favicon:latest . + + - name: DockerHub login + uses: docker/login-action@v2 + with: + username: ${{ vars.DOCKERHUB_USERNAME}} + password: ${{ vars.DOCKERHUB_PASSWORD}} + + - name: Push Docker image + run: | + docker push ${{ vars.DOCKERHUB_USERNAME}}/erica-favicon:latest + + - name: Deploy to EC2 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.EC2_HOST }} + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + script: | + docker pull ${{ vars.DOCKERHUB_USERNAME }}/erica-favicon:latest + docker rm -f back 2>/dev/null || true + docker compose -f docker-compose.yml --env-file .env up --build -d \ No newline at end of file diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml new file mode 100644 index 0000000..38ccd1e --- /dev/null +++ b/.github/workflows/build_test.yml @@ -0,0 +1,30 @@ +name: PR Build TEST + +on: + pull_request: + branches: [ main, develop ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 + with: + cache-read-only: false + + - name: Grant Execute Permission For Gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: | + set -e + ./gradlew clean build -Dspring.profiles.active=local diff --git a/Dockerfile b/Dockerfile index b596bbd..b2bbc88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,6 @@ -FROM gradle:8.4-jdk17 AS builder -WORKDIR /app -COPY . . -RUN ./gradlew bootJar - -FROM amazoncorretto:17.0.12 -WORKDIR /app -COPY --from=builder /app/build/libs/*.jar app.jar -ENTRYPOINT ["java", "-jar", "-Djava.security.egd=file:/dev/./urandom", "app.jar"] \ No newline at end of file +FROM amazoncorretto:17 +RUN yum update -y && \ + yum install -y python3 && \ + yum clean all +COPY build/libs/app.jar app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8d7024c..c2f1e10 100644 --- a/build.gradle +++ b/build.gradle @@ -29,38 +29,53 @@ jar { enabled = false } +springBoot { + mainClass = 'com.capstone.favicon.FaviconApplication' +} + +bootJar { + archiveFileName = 'app.jar' +} + +jar { + enabled = false +} + dependencies { - implementation 'org.springframework.boot:spring-boot-starter-jdbc' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'org.springframework.boot:spring-boot-starter:3.4.2' - implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'io.netty:netty-resolver-dns-native-macos:4.1.109.Final:osx-aarch_64' + + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter:3.4.2' + implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.postgresql:postgresql:42.6.0' + implementation 'org.postgresql:postgresql:42.6.0' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - // lombok + // lombok compileOnly 'org.projectlombok:lombok:1.18.28' - annotationProcessor 'org.projectlombok:lombok:1.18.28' - testAnnotationProcessor 'org.projectlombok:lombok:1.18.36' + annotationProcessor 'org.projectlombok:lombok:1.18.28' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.36' implementation 'org.springframework.boot:spring-boot-starter-security' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-starter-test' - // aws - implementation 'software.amazon.awssdk:s3:2.20.28' + // aws + implementation 'software.amazon.awssdk:s3:2.20.28' - // assertj - testImplementation "org.assertj:assertj-core:3.26.3" - testImplementation "org.junit.jupiter:junit-jupiter:5.11.4" + // assertj + testImplementation "org.assertj:assertj-core:3.26.3" + testImplementation "org.junit.jupiter:junit-jupiter:5.11.4" - // mail - implementation 'org.springframework.boot:spring-boot-starter-mail' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + // mail + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' } tasks.named('test') { - useJUnitPlatform() -} \ No newline at end of file + useJUnitPlatform() +} diff --git a/docker-compose.yml b/docker-compose.yml index b5576d0..a5a7b62 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,37 +1,24 @@ services: - postgres: - image: postgres - container_name: postgres - restart: always - environment: - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: ${POSTGRES_DB} + redis: + image: redis:latest + container_name: redis ports: - - "5432:5432" + - "6379:6379" + command: + - redis-server + networks: + - backend-network backend: - build: - context: ${REPO_BACK_URL} - container_name: back - restart: always - depends_on: - - postgres - environment: - SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL} - SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME} - SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} + image: 211i2/erica-backend:latest + container_name: backend ports: - - "8081:8080" - - frontend: - build: - context: ${REPO_FRONT_URL} - container_name: front - restart: always + - "8080:8080" + networks: + - backend-network depends_on: - - backend - environment: - REACT_APP_API_URL: ${REACT_APP_API_URL} - ports: - - "3000:80" + - redis + +networks: + backend-network: + driver: bridge diff --git a/src/main/java/com/capstone/favicon/FaviconApplication.java b/src/main/java/com/capstone/favicon/FaviconApplication.java index 90f8bb4..9e5c134 100644 --- a/src/main/java/com/capstone/favicon/FaviconApplication.java +++ b/src/main/java/com/capstone/favicon/FaviconApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class FaviconApplication { public static void main(String[] args) { diff --git a/src/main/java/com/capstone/favicon/admin/application/AdminServiceImpl.java b/src/main/java/com/capstone/favicon/admin/application/AdminServiceImpl.java new file mode 100644 index 0000000..e6f3776 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/application/AdminServiceImpl.java @@ -0,0 +1,22 @@ +package com.capstone.favicon.admin.application; + +import com.capstone.favicon.admin.application.service.AdminService; +import com.capstone.favicon.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class AdminServiceImpl implements AdminService { + + @Autowired + private UserRepository userRepository; + + @Override + @Transactional + public void deleteUser(Long userId) { + userRepository.deleteByUserId(userId); + } +} diff --git a/src/main/java/com/capstone/favicon/admin/application/FAQServiceImpl.java b/src/main/java/com/capstone/favicon/admin/application/FAQServiceImpl.java new file mode 100644 index 0000000..71c55d8 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/application/FAQServiceImpl.java @@ -0,0 +1,80 @@ +package com.capstone.favicon.admin.application; + +import com.capstone.favicon.admin.application.service.FAQService; +import com.capstone.favicon.admin.domain.FAQ; +import com.capstone.favicon.admin.dto.FAQRequestDto; +import com.capstone.favicon.admin.dto.FAQResponseDto; +import com.capstone.favicon.admin.repository.FAQRepository; +import com.capstone.favicon.user.domain.User; +import com.capstone.favicon.user.repository.UserRepository; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Primary +@RequiredArgsConstructor +public class FAQServiceImpl implements FAQService { + + private final FAQRepository faqRepository; + private final UserRepository userRepository; + + @Override + public User getAdminUserFromSession(HttpServletRequest request) { + HttpSession session = request.getSession(); + Long id = (Long) session.getAttribute("id"); + User user = userRepository.findByUserId(id); + if (user == null || user.getRole() != 1) { + throw new RuntimeException("이 기능은 관리자만 접근 가능합니다."); + } + return user; + } + + @Override + public void createFAQ(FAQRequestDto request) { + FAQ faq = new FAQ(); + faq.setCategory(request.getCategory()); + faq.setQuestion(request.getQuestion()); + faq.setAnswer(request.getAnswer()); + + faqRepository.save(faq); + } + + @Override + public void updateFAQ(Long faqId, FAQRequestDto request) { + FAQ faq = faqRepository.findById(faqId) + .orElseThrow(() -> new RuntimeException("FAQ를 찾을 수 없습니다.")); + + faq.setCategory(request.getCategory()); + faq.setQuestion(request.getQuestion()); + faq.setAnswer(request.getAnswer()); + + faqRepository.save(faq); + } + + @Override + public void deleteFAQ(Long faqId) { + FAQ faq = faqRepository.findById(faqId) + .orElseThrow(() -> new RuntimeException("FAQ를 찾을 수 없습니다.")); + faqRepository.delete(faq); + } + + @Override + public List getAllFAQs() { + List faqs = faqRepository.findAll(); + return faqs.stream().map(FAQResponseDto::new).collect(Collectors.toList()); + } + + @Override + public FAQResponseDto getFAQById(Long faqId) { + FAQ faq = faqRepository.findById(faqId) + .orElseThrow(() -> new RuntimeException("FAQ를 찾을 수 없습니다.")); + return new FAQResponseDto(faq); + } + +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/admin/application/NoticeServiceImpl.java b/src/main/java/com/capstone/favicon/admin/application/NoticeServiceImpl.java new file mode 100644 index 0000000..0b9f098 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/application/NoticeServiceImpl.java @@ -0,0 +1,117 @@ +package com.capstone.favicon.admin.application; + +import com.capstone.favicon.admin.application.service.FAQService; +import com.capstone.favicon.admin.application.service.NoticeService; +import com.capstone.favicon.admin.domain.Notice; +import com.capstone.favicon.admin.dto.NoticeRequestDto; +import com.capstone.favicon.admin.dto.NoticeResponseDto; +import com.capstone.favicon.admin.repository.NoticeRepository; +import com.capstone.favicon.admin.domain.FAQ; +import com.capstone.favicon.admin.dto.FAQRequestDto; +import com.capstone.favicon.admin.dto.FAQResponseDto; +import com.capstone.favicon.admin.repository.FAQRepository; +import com.capstone.favicon.user.domain.User; +import com.capstone.favicon.user.repository.UserRepository; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class NoticeServiceImpl implements NoticeService { + + private final NoticeRepository noticeRepository; + private final UserRepository userRepository; + + @Override + public User getAdminUserFromSession(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session == null) { + throw new RuntimeException("세션이 존재하지 않습니다."); + } + + Long id = (Long) session.getAttribute("id"); + if (id == null) { + throw new RuntimeException("세션에 로그인 정보가 없습니다."); + } + + User user = userRepository.findByUserId(id); + if (user == null || user.getRole() != 1) { + throw new RuntimeException("이 기능은 관리자만 접근 가능합니다."); + } + return user; + } + + @Override + public void createNotice(NoticeRequestDto request) { + Notice notice = new Notice(); + notice.setTitle(request.getTitle()); + notice.setContent(request.getContent()); + notice.setLabel(request.getLabel()); + notice.setUpdateDate(LocalDate.now()); + + noticeRepository.save(notice); + } + + @Override + public void updateNotice(Long noticeId, NoticeRequestDto request) { + Notice notice = noticeRepository.findById(noticeId).orElseThrow(() -> new RuntimeException("공지사항을 찾을 수 없습니다.")); + notice.setTitle(request.getTitle()); + notice.setContent(request.getContent()); + notice.setLabel(request.getLabel()); + notice.setUpdateDate(LocalDate.now()); + noticeRepository.save(notice); + } + + @Override + public void deleteNotice(Long noticeId) { + Notice notice = noticeRepository.findById(noticeId).orElseThrow(() -> new RuntimeException("공지사항을 찾을 수 없습니다.")); + noticeRepository.delete(notice); + } + + @Override + public List getAllNotices() { + List notices = noticeRepository.findAll(); + return notices.stream() + .map(notice -> new NoticeResponseDto( + notice.getNoticeId(), + notice.getTitle(), + notice.getContent(), + notice.getCreateDate().toString(), + notice.getView(), + notice.getLabel().name() + )) + .collect(Collectors.toList()); + } + + @Override + public NoticeResponseDto getNoticeById(Long noticeId) { + Notice notice = noticeRepository.findById(noticeId) + .orElseThrow(() -> new RuntimeException("공지사항을 찾을 수 없습니다.")); + + return new NoticeResponseDto( + notice.getNoticeId(), + notice.getTitle(), + notice.getContent(), + notice.getCreateDate().toString(), + notice.getView(), + notice.getLabel().name() + ); + } + + @Override + public Notice getNotice(Long noticeId) { + Notice notice = noticeRepository.findById(noticeId) + .orElseThrow(() -> new IllegalArgumentException("해당 공지가 없습니다. ID: " + noticeId)); + + notice.incrementView(); + + return noticeRepository.save(notice); + } + +} diff --git a/src/main/java/com/capstone/favicon/admin/application/StatisticsServiceImpl.java b/src/main/java/com/capstone/favicon/admin/application/StatisticsServiceImpl.java new file mode 100644 index 0000000..6cce1a0 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/application/StatisticsServiceImpl.java @@ -0,0 +1,66 @@ +package com.capstone.favicon.admin.application; + + +import com.capstone.favicon.admin.application.service.StatisticsService; +import com.capstone.favicon.admin.dto.MonthlyCountDto; +import com.capstone.favicon.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +@Service +public class StatisticsServiceImpl implements StatisticsService { + + @Autowired + private UserRepository userRepository; + + @Override + public Map getUserCount() { + LocalDateTime today = LocalDateTime.now(); + LocalDateTime start = today.minusMonths(1).withDayOfMonth(1); + LocalDateTime end = start.plusMonths(1); + + int previousCount = userRepository.countUsersAt(start, end); + int currentCount = userRepository.countUsersAt(end, today.plusDays(1)); + + double rate = 0.0; + if (previousCount > 0) { + rate = ((double) (currentCount-previousCount) / previousCount) *100.0; + rate = Math.round(rate*10.0)/10.0; + } else { + rate = 100.0; + } + + Map result = new HashMap<>(); + result.put("total", userRepository.countAllUsers()); + result.put("rate", rate); + return result; + } + + @Override + public List getUserOverview() { + LocalDateTime end = LocalDateTime.now().plusMonths(1).withDayOfMonth(1); + List result = new ArrayList<>(); + for (int i = 0; i<6; i++) { + LocalDateTime start = end.minusMonths(1); + int month = start.getMonthValue(); + int count = userRepository.countUsersAt(start, end); + result.add(new MonthlyCountDto(month, count)); + end = start; + } + return result; + } + + @Override + public List getAllUsers() { + return userRepository.getAll(); + } + +} diff --git a/src/main/java/com/capstone/favicon/admin/application/service/AdminService.java b/src/main/java/com/capstone/favicon/admin/application/service/AdminService.java new file mode 100644 index 0000000..18f0fc2 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/application/service/AdminService.java @@ -0,0 +1,5 @@ +package com.capstone.favicon.admin.application.service; + +public interface AdminService { + void deleteUser(Long userId); +} diff --git a/src/main/java/com/capstone/favicon/admin/application/service/FAQService.java b/src/main/java/com/capstone/favicon/admin/application/service/FAQService.java new file mode 100644 index 0000000..63c2421 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/application/service/FAQService.java @@ -0,0 +1,19 @@ +package com.capstone.favicon.admin.application.service; + +import com.capstone.favicon.admin.dto.FAQRequestDto; +import com.capstone.favicon.admin.dto.FAQResponseDto; +import com.capstone.favicon.user.domain.User; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.List; + +public interface FAQService { + User getAdminUserFromSession(HttpServletRequest request); + + void createFAQ(FAQRequestDto request); + void updateFAQ(Long faqId, FAQRequestDto request); + void deleteFAQ(Long faqId); + List getAllFAQs(); + FAQResponseDto getFAQById(Long faqId); +} + diff --git a/src/main/java/com/capstone/favicon/admin/application/service/NoticeService.java b/src/main/java/com/capstone/favicon/admin/application/service/NoticeService.java new file mode 100644 index 0000000..3c300c5 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/application/service/NoticeService.java @@ -0,0 +1,20 @@ +package com.capstone.favicon.admin.application.service; + +import com.capstone.favicon.admin.domain.Notice; +import com.capstone.favicon.admin.dto.NoticeRequestDto; +import com.capstone.favicon.admin.dto.NoticeResponseDto; +import com.capstone.favicon.user.domain.User; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.List; + +public interface NoticeService { + User getAdminUserFromSession(HttpServletRequest request); + + void createNotice(NoticeRequestDto request); + void updateNotice(Long noticeId, NoticeRequestDto request); + void deleteNotice(Long noticeId); + List getAllNotices(); + NoticeResponseDto getNoticeById(Long noticeId); + Notice getNotice(Long noticeId); +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/admin/application/service/StatisticsService.java b/src/main/java/com/capstone/favicon/admin/application/service/StatisticsService.java new file mode 100644 index 0000000..0620da5 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/application/service/StatisticsService.java @@ -0,0 +1,12 @@ +package com.capstone.favicon.admin.application.service; + +import com.capstone.favicon.admin.dto.MonthlyCountDto; + +import java.util.List; +import java.util.Map; + +public interface StatisticsService { + Map getUserCount(); + List getUserOverview(); + List getAllUsers(); +} diff --git a/src/main/java/com/capstone/favicon/admin/controller/AdminController.java b/src/main/java/com/capstone/favicon/admin/controller/AdminController.java new file mode 100644 index 0000000..dac55ad --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/controller/AdminController.java @@ -0,0 +1,32 @@ +package com.capstone.favicon.admin.controller; + +import com.capstone.favicon.admin.application.service.AdminService; +import com.capstone.favicon.config.APIResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + + +@RequiredArgsConstructor +@RestController +@RequestMapping("/admin") +public class AdminController { + + @Autowired + private AdminService adminService; + + @DeleteMapping("/delete-user") + public ResponseEntity> deleteUser(@RequestParam("userId") Long userId) { + try { + adminService.deleteUser(userId); + return ResponseEntity.ok().body(APIResponse.successAPI("탈퇴시켰습니다.", userId)); + } catch (Exception e) { + String message = e.getMessage(); + return ResponseEntity.badRequest().body(APIResponse.errorAPI(message)); + } + } +} diff --git a/src/main/java/com/capstone/favicon/admin/controller/FAQController.java b/src/main/java/com/capstone/favicon/admin/controller/FAQController.java new file mode 100644 index 0000000..ca7e3cf --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/controller/FAQController.java @@ -0,0 +1,72 @@ +package com.capstone.favicon.admin.controller; + +import com.capstone.favicon.admin.application.service.FAQService; +import com.capstone.favicon.admin.dto.FAQRequestDto; +import com.capstone.favicon.admin.dto.FAQResponseDto; +import com.capstone.favicon.config.APIResponse; +import com.capstone.favicon.user.domain.User; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/faq") +@RequiredArgsConstructor +public class FAQController { + + private final FAQService faqService; + + @PostMapping("/create") + public ResponseEntity> createFAQ(@RequestBody FAQRequestDto request) { + try { + faqService.createFAQ(request); + return ResponseEntity.ok().body(APIResponse.successAPI("관리자가 FAQ를 생성하였습니다.", null)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @PutMapping("/{faqId}") + public ResponseEntity> updateFAQ(@PathVariable Long faqId, @RequestBody FAQRequestDto request) { + try { + faqService.updateFAQ(faqId, request); + return ResponseEntity.ok().body(APIResponse.successAPI("관리자가 FAQ를 수정했습니다.", null)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @DeleteMapping("/{faqId}") + public ResponseEntity> deleteFAQ(@PathVariable Long faqId) { + try { + faqService.deleteFAQ(faqId); + return ResponseEntity.ok().body(APIResponse.successAPI("관리자가 FAQ를 삭제했습니다.", null)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/list") + public ResponseEntity> getAllFAQs() { + try { + List faqs = faqService.getAllFAQs(); + return ResponseEntity.ok().body(APIResponse.successAPI("success", faqs)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/{faqId}") + public ResponseEntity> getFAQById(@PathVariable Long faqId) { + try { + FAQResponseDto faq = faqService.getFAQById(faqId); + return ResponseEntity.ok().body(APIResponse.successAPI("success", faq)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + +} diff --git a/src/main/java/com/capstone/favicon/admin/controller/NoticeController.java b/src/main/java/com/capstone/favicon/admin/controller/NoticeController.java new file mode 100644 index 0000000..bce1b11 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/controller/NoticeController.java @@ -0,0 +1,82 @@ +package com.capstone.favicon.admin.controller; + +import com.capstone.favicon.admin.application.service.NoticeService; +import com.capstone.favicon.admin.domain.Notice; +import com.capstone.favicon.admin.dto.NoticeRequestDto; +import com.capstone.favicon.admin.dto.NoticeResponseDto; +import com.capstone.favicon.config.APIResponse; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +@RestController +@RequestMapping("/notice") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + @PostMapping("/create") + public ResponseEntity> createNotice(@RequestBody NoticeRequestDto request) { + try { + noticeService.createNotice(request); + return ResponseEntity.ok().body(APIResponse.successAPI("공지사항이 등록되었습니다.", null)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @PutMapping("/{noticeId}") + public ResponseEntity> updateNotice(@PathVariable Long noticeId, @RequestBody NoticeRequestDto request) { + try { + noticeService.updateNotice(noticeId, request); + return ResponseEntity.ok().body(APIResponse.successAPI("공지사항이 수정되었습니다.", null)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @DeleteMapping("/{noticeId}") + public ResponseEntity> deleteNotice(@PathVariable Long noticeId) { + try { + noticeService.deleteNotice(noticeId); + return ResponseEntity.ok().body(APIResponse.successAPI("공지사항이 삭제되었습니다.", null)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/list") + public ResponseEntity> getAllNotices() { + try { + List notices = noticeService.getAllNotices(); + return ResponseEntity.ok().body(APIResponse.successAPI("success", notices)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/{noticeId}") + public ResponseEntity> getNoticeById(@PathVariable Long noticeId) { + try { + NoticeResponseDto notice = noticeService.getNoticeById(noticeId); + return ResponseEntity.ok().body(APIResponse.successAPI("success", notice)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/view/{noticeId}") + public ResponseEntity> getNotice(@PathVariable Long noticeId) { + try { + Notice notice = noticeService.getNotice(noticeId); + return ResponseEntity.ok().body(APIResponse.successAPI("success", notice)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } +} diff --git a/src/main/java/com/capstone/favicon/admin/controller/StatisticsController.java b/src/main/java/com/capstone/favicon/admin/controller/StatisticsController.java new file mode 100644 index 0000000..e3498f1 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/controller/StatisticsController.java @@ -0,0 +1,54 @@ +package com.capstone.favicon.admin.controller; + +import com.capstone.favicon.admin.application.service.StatisticsService; +import com.capstone.favicon.admin.dto.MonthlyCountDto; +import com.capstone.favicon.config.APIResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +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; +import java.util.Map; + + +@RequiredArgsConstructor +@RestController +@RequestMapping("/statistics") +public class StatisticsController { + + @Autowired + private StatisticsService statisticsService; + + @GetMapping("/user-stats") + public ResponseEntity> getUserStats() { + try { + Map userStats = statisticsService.getUserCount(); + return ResponseEntity.ok().body(APIResponse.successAPI("전체 사용자 & 지난달 대비 증가추이 비율", userStats)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/user-overview") + public ResponseEntity> getUserOverview() { + try { + List userOverview = statisticsService.getUserOverview(); + return ResponseEntity.ok().body(APIResponse.successAPI("사용자 개요", userOverview)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/all-user") + public ResponseEntity> getAllUser() { + try { + List users = statisticsService.getAllUsers(); + return ResponseEntity.ok().body(APIResponse.successAPI("전체 사용자", users)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } +} diff --git a/src/main/java/com/capstone/favicon/admin/domain/FAQ.java b/src/main/java/com/capstone/favicon/admin/domain/FAQ.java new file mode 100644 index 0000000..4839b1c --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/domain/FAQ.java @@ -0,0 +1,32 @@ +package com.capstone.favicon.admin.domain; + +import com.capstone.favicon.user.domain.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "faq") +public class FAQ { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long faqId; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private FAQCategory category; + + @Column(nullable = false, length = 255) + private String question; + + @Column(nullable = false, length = 255) + private String answer; + + public enum FAQCategory { + 기타, 데이터라이선스, 문제해결, 서비스이용, 회원정보관리 + } + +} diff --git a/src/main/java/com/capstone/favicon/admin/domain/Notice.java b/src/main/java/com/capstone/favicon/admin/domain/Notice.java new file mode 100644 index 0000000..d18716c --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/domain/Notice.java @@ -0,0 +1,53 @@ +package com.capstone.favicon.admin.domain; + +import com.capstone.favicon.user.domain.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; + +@Getter +@Setter +@Entity +@Table(name = "notice") +public class Notice { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long noticeId; + + @Column(nullable = false) + private String title; + + @Column(nullable = false, length = 255) + private String content; + + @Column(nullable = false) + private LocalDate createDate = LocalDate.now(); + + private LocalDate updateDate; + + @Column(nullable = false) + private Integer view = 0; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private NoticeLabel label = NoticeLabel.일반; // 공지 유형 (일반, 업데이트, 중요) + + public void update(String title, String content, NoticeLabel label) { + this.title = title; + this.content = content; + this.label = label; + this.updateDate = LocalDate.now(); + } + + public void incrementView() { + this.view += 1; + } + + public enum NoticeLabel { + 일반, 업데이트, 중요 + } + +} diff --git a/src/main/java/com/capstone/favicon/admin/dto/FAQRequestDto.java b/src/main/java/com/capstone/favicon/admin/dto/FAQRequestDto.java new file mode 100644 index 0000000..1ee20a0 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/dto/FAQRequestDto.java @@ -0,0 +1,13 @@ +package com.capstone.favicon.admin.dto; + +import com.capstone.favicon.admin.domain.FAQ.FAQCategory; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class FAQRequestDto { + private FAQCategory category; + private String question; + private String answer; +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/admin/dto/FAQResponseDto.java b/src/main/java/com/capstone/favicon/admin/dto/FAQResponseDto.java new file mode 100644 index 0000000..4684177 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/dto/FAQResponseDto.java @@ -0,0 +1,19 @@ +package com.capstone.favicon.admin.dto; + +import com.capstone.favicon.admin.domain.FAQ; +import lombok.Getter; + +@Getter +public class FAQResponseDto { + private Long faqId; + private String category; + private String question; + private String answer; + + public FAQResponseDto(FAQ faq) { + this.faqId = faq.getFaqId(); + this.category = faq.getCategory().name(); + this.question = faq.getQuestion(); + this.answer = faq.getAnswer(); + } +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/admin/dto/MonthlyCountDto.java b/src/main/java/com/capstone/favicon/admin/dto/MonthlyCountDto.java new file mode 100644 index 0000000..9143b22 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/dto/MonthlyCountDto.java @@ -0,0 +1,14 @@ +package com.capstone.favicon.admin.dto; + +import lombok.Getter; + +@Getter +public class MonthlyCountDto { + private int month; + private int count; + + public MonthlyCountDto(int month, int count) { + this.month = month; + this.count = count; + } +} diff --git a/src/main/java/com/capstone/favicon/admin/dto/NoticeRequestDto.java b/src/main/java/com/capstone/favicon/admin/dto/NoticeRequestDto.java new file mode 100644 index 0000000..3a52043 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/dto/NoticeRequestDto.java @@ -0,0 +1,13 @@ +package com.capstone.favicon.admin.dto; + +import com.capstone.favicon.admin.domain.Notice; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class NoticeRequestDto { + private String title; + private String content; + private Notice.NoticeLabel label = Notice.NoticeLabel.일반; +} diff --git a/src/main/java/com/capstone/favicon/admin/dto/NoticeResponseDto.java b/src/main/java/com/capstone/favicon/admin/dto/NoticeResponseDto.java new file mode 100644 index 0000000..3f4ea51 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/dto/NoticeResponseDto.java @@ -0,0 +1,15 @@ +package com.capstone.favicon.admin.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class NoticeResponseDto { + private Long noticeId; + private String title; + private String content; + private String createDate; + private int view; + private String label; +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/admin/repository/FAQRepository.java b/src/main/java/com/capstone/favicon/admin/repository/FAQRepository.java new file mode 100644 index 0000000..bea430f --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/repository/FAQRepository.java @@ -0,0 +1,9 @@ +package com.capstone.favicon.admin.repository; + +import com.capstone.favicon.admin.domain.FAQ; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface FAQRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/admin/repository/NoticeRepository.java b/src/main/java/com/capstone/favicon/admin/repository/NoticeRepository.java new file mode 100644 index 0000000..e61d8e0 --- /dev/null +++ b/src/main/java/com/capstone/favicon/admin/repository/NoticeRepository.java @@ -0,0 +1,9 @@ +package com.capstone.favicon.admin.repository; + +import com.capstone.favicon.admin.domain.Notice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface NoticeRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/aws/MetadataParser.java b/src/main/java/com/capstone/favicon/aws/MetadataParser.java new file mode 100644 index 0000000..433d775 --- /dev/null +++ b/src/main/java/com/capstone/favicon/aws/MetadataParser.java @@ -0,0 +1,74 @@ +package com.capstone.favicon.aws; + +import com.capstone.favicon.dataset.domain.DatasetTheme; +import lombok.Getter; + +import java.util.List; + +public class MetadataParser { + + public static DatasetMetadata extractMetadata(String fileName, List themes) { + + if (!fileName.startsWith("preprocessing/")) { + throw new IllegalArgumentException("preprocessing 폴더 내 파일이 아닙니다: " + fileName); + } + + String pureFileName = fileName.substring(fileName.lastIndexOf("/") + 1); + System.out.println("=== [DEBUG] 메타데이터 파싱용 파일명: " + pureFileName); + String[] parts = pureFileName.split("_"); + + if (parts.length < 4) { + throw new IllegalArgumentException("파일명 형식이 올바르지 않습니다: " + fileName); + } + + String themeName = parts[0]; + String datasetName = parts[1]; + String organization = parts[2]; + String type = extractFileType(pureFileName); + + String titleWithoutExtension = pureFileName.substring(0, pureFileName.lastIndexOf('.')); + String[] titleParts = titleWithoutExtension.split("_"); + + String region = titleParts[titleParts.length - 1]; + + String subCategory = titleParts.length > 2 ? titleParts[1] : ""; + + Long datasetThemeId = themes.stream() + .filter(t -> t.getTheme().equals(themeName)) + .map(DatasetTheme::getDatasetThemeId) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("해당 카테고리는 존재하지 않습니다: " + themeName)); + + String description = organization + "에서 수집한 " + region + "의 " + subCategory + " 데이터 입니다"; + + + return new DatasetMetadata(datasetThemeId, datasetName, titleWithoutExtension, organization, type, description); + } + + private static String extractFileType(String fileName) { + int lastDotIndex = fileName.lastIndexOf("."); + if (lastDotIndex == -1 || lastDotIndex == fileName.length() - 1) { + return "UNKNOWN"; + } + return fileName.substring(lastDotIndex + 1).toUpperCase(); + } + + @Getter + public static class DatasetMetadata { + private final Long datasetThemeId; + private final String name; + private final String title; + private final String organization; + private final String type; + private final String description; + + public DatasetMetadata(Long datasetThemeId, String name, String title, String organization, String type, String description) { + this.datasetThemeId = datasetThemeId; + this.name = name; + this.title = title; + this.organization = organization; + this.type = type; + this.description = description; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/aws/S3Controller.java b/src/main/java/com/capstone/favicon/aws/S3Controller.java new file mode 100644 index 0000000..364318e --- /dev/null +++ b/src/main/java/com/capstone/favicon/aws/S3Controller.java @@ -0,0 +1,121 @@ +package com.capstone.favicon.aws; + +import com.capstone.favicon.config.S3Config; +import com.capstone.favicon.dataset.domain.Dataset; +import com.capstone.favicon.dataset.domain.FileExtension; +import com.capstone.favicon.dataset.repository.DatasetRepository; +import com.capstone.favicon.dataset.domain.DatasetTheme; +import com.capstone.favicon.dataset.domain.Resource; +import com.capstone.favicon.dataset.repository.DatasetThemeRepository; +import com.capstone.favicon.dataset.repository.ResourceRepository; +import com.capstone.favicon.aws.MetadataParser.DatasetMetadata; +import com.capstone.favicon.config.APIResponse; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("/s3") +public class S3Controller { + + private final S3Config s3Config; + private final DatasetRepository datasetRepository; + private final DatasetThemeRepository datasetThemeRepository; + private final ResourceRepository resourceRepository; + + public S3Controller(@Qualifier("s3Config") S3Config s3Config, DatasetRepository datasetRepository, + DatasetThemeRepository datasetThemeRepository, ResourceRepository resourceRepository) { + this.s3Config = s3Config; + this.datasetRepository = datasetRepository; + this.datasetThemeRepository = datasetThemeRepository; + this.resourceRepository = resourceRepository; + } + + @PostMapping("/upload") + public ResponseEntity> uploadFile(@RequestParam("files") MultipartFile[] files) { + try { + for (MultipartFile file : files) { + if (file.isEmpty() || file.getOriginalFilename() == null || file.getOriginalFilename().trim().isEmpty()) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI("파일 이름이 올바르지 않습니다.")); + } + + String originalFileName = file.getOriginalFilename().trim(); + String directory = "preprocessing"; + String fileUrl = s3Config.uploadFile(file, directory); + + List datasetThemes = datasetThemeRepository.findAll(); + String s3FileName = directory + "/" + originalFileName; + DatasetMetadata metadata = MetadataParser.extractMetadata(s3FileName, datasetThemes); + + DatasetTheme datasetTheme = datasetThemes.stream() + .filter(theme -> theme.getDatasetThemeId().equals(metadata.getDatasetThemeId())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("해당 dataset_theme_id가 존재하지 않습니다: " + metadata.getDatasetThemeId())); + + Dataset dataset = datasetRepository + .findByDatasetThemeAndNameAndOrganization(datasetTheme, metadata.getName(), metadata.getOrganization()) + .orElseGet(() -> { + LocalDate lastModified = s3Config.getLastModifiedDate(s3FileName); + return datasetRepository.save(new Dataset( + datasetTheme, + metadata.getName(), + metadata.getTitle(), + metadata.getOrganization(), + metadata.getDescription(), + s3FileName, + LocalDate.now(), + lastModified, + 0, + 0 + )); + }); + + FileExtension type = FileExtension.valueOf(metadata.getType()); + Resource resource = new Resource(dataset, originalFileName, type, fileUrl); + resourceRepository.save(resource); + } + + return ResponseEntity.ok(APIResponse.successAPI("파일이 업로드되었습니다", null)); + + } catch (IllegalArgumentException | IOException e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } catch (Exception e) { + return ResponseEntity.internalServerError().body(APIResponse.errorAPI("서버 오류: " + e.getMessage())); + } + } + + @Transactional + @DeleteMapping("/delete/{resourceId}") + public ResponseEntity> deleteFile(@PathVariable Long resourceId) { + try { + Resource resource = resourceRepository.findById(resourceId) + .orElseThrow(() -> new IllegalArgumentException("Resource를 찾을 수 없습니다: " + resourceId)); + + Dataset dataset = resource.getDataset(); + + s3Config.deleteFile(resource.getResourceUrl()); + dataset.setResource(null); + + resourceRepository.delete(resource); + resourceRepository.flush(); + + if (datasetRepository.existsById(dataset.getDatasetId()) && dataset.getResource() == null) { + datasetRepository.delete(dataset); + datasetRepository.flush(); + } + + return ResponseEntity.ok(APIResponse.successAPI("파일이 삭제되었습니다", null)); + + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } catch (Exception e) { + return ResponseEntity.internalServerError().body(APIResponse.errorAPI("서버 오류: " + e.getMessage())); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/aws/S3MetadataSyncService.java b/src/main/java/com/capstone/favicon/aws/S3MetadataSyncService.java new file mode 100644 index 0000000..02b00b1 --- /dev/null +++ b/src/main/java/com/capstone/favicon/aws/S3MetadataSyncService.java @@ -0,0 +1,114 @@ +package com.capstone.favicon.aws; + +import com.capstone.favicon.config.S3Config; +import com.capstone.favicon.dataset.domain.Dataset; +import com.capstone.favicon.dataset.domain.DatasetTheme; +import com.capstone.favicon.dataset.domain.Resource; +import com.capstone.favicon.dataset.repository.DatasetRepository; +import com.capstone.favicon.dataset.repository.DatasetThemeRepository; +import com.capstone.favicon.aws.MetadataParser.DatasetMetadata; +import com.capstone.favicon.dataset.repository.ResourceRepository; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import com.capstone.favicon.dataset.domain.FileExtension; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +@Service +public class S3MetadataSyncService { + private final S3Config s3Config; + private final DatasetRepository datasetRepository; + private final DatasetThemeRepository datasetThemeRepository; + private final ResourceRepository resourceRepository; + + public S3MetadataSyncService(@Qualifier("s3Config") S3Config s3Config, DatasetRepository datasetRepository, DatasetThemeRepository datasetThemeRepository, ResourceRepository resourceRepository) { + this.s3Config = s3Config; + this.datasetRepository = datasetRepository; + this.datasetThemeRepository = datasetThemeRepository; + this.resourceRepository = resourceRepository; + } + + @Transactional + @Scheduled(fixedRate = 600000000) + public void syncS3FilesToDB() { + List fileNames = s3Config.listFilesInBucket(); + List datasetThemes = datasetThemeRepository.findAll(); + + for (String fileName : fileNames) { + try { + System.out.println("=== [디버깅] S3에서 가져온 전체 파일 경로: " + fileName); + DatasetMetadata metadata = MetadataParser.extractMetadata(fileName, datasetThemes); + + Dataset datasetToUse = datasetRepository.findByS3Key(fileName).orElse(null); + + if (datasetToUse == null) { + Dataset dataset = new Dataset( + datasetThemes.stream().filter(theme -> theme.getDatasetThemeId().equals(metadata.getDatasetThemeId())).findFirst().orElse(null), + metadata.getName(), metadata.getTitle(), metadata.getOrganization(), metadata.getDescription() + ); + + LocalDate lastModified = s3Config.getLastModifiedDate(fileName); + dataset.setUpdateDate(lastModified); + //dataset.setUpdateDate(LocalDate.now()); + dataset.setDownload(0); + dataset.setView(0); + dataset.setCreatedDate(LocalDate.now()); + dataset.setS3Key(fileName); + + datasetRepository.save(dataset); + datasetToUse = dataset; + System.out.println("새로운 Dataset 테이블 추가됨: " + metadata.getName()); + + String rawFileName = fileName.substring(fileName.lastIndexOf("/") + 1); + String fileUrl = s3Config.generateFileUrl(fileName); + System.out.println("[디버깅] Resource 저장 시도 - dataset: " + datasetToUse.getName() + ", resourceName: " + rawFileName + ", url: " + fileUrl); + Resource newResource = new Resource(datasetToUse, rawFileName, FileExtension.CSV, fileUrl); + resourceRepository.save(newResource); + System.out.println("[디버깅] Resource 저장 완료: " + rawFileName); + } else { + + String rawFileName = fileName.substring(fileName.lastIndexOf("/") + 1); + Optional optionalResourceFromDB = resourceRepository.findByDatasetAndResourceName(datasetToUse, rawFileName); + + if (optionalResourceFromDB.isPresent()) { + System.out.println("[정보] Resource 이미 존재함: " + rawFileName); + } else { + String fileUrl = s3Config.generateFileUrl(fileName); + Resource newResource = new Resource(datasetToUse, rawFileName, FileExtension.CSV, fileUrl); + resourceRepository.save(newResource); + System.out.println("[정보] 기존 Dataset에 새로운 Resource 저장: " + rawFileName); + } + } + + } catch (Exception e) { + System.err.println("메타데이터 추출 중 오류 발생: " + fileName); + e.printStackTrace(); + } + } + + try { + ProcessBuilder processBuilder = new ProcessBuilder("venv/bin/python3", "s3_rds.py"); + processBuilder.redirectErrorStream(true); + Process process = processBuilder.start(); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + System.out.println("[Python] " + line); + } + } + + int exitCode = process.waitFor(); + System.out.println("Python 스크립트 종료 코드: " + exitCode); + } catch (Exception e) { + System.err.println("Python 스크립트 실행 중 오류 발생"); + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/capstone/favicon/config/APIResponse.java b/src/main/java/com/capstone/favicon/config/APIResponse.java new file mode 100644 index 0000000..1ef65a7 --- /dev/null +++ b/src/main/java/com/capstone/favicon/config/APIResponse.java @@ -0,0 +1,30 @@ +package com.capstone.favicon.config; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class APIResponse { + + private static final String SUCCESS_STATUS = "success"; + private static final String ERROR_STATUS = "error"; + + private String status; + private String message; + private T data; + + public static APIResponse successAPI(String message, T data) { + return new APIResponse<>(SUCCESS_STATUS, message, data); + } + + public static APIResponse errorAPI(String message) { + return new APIResponse<>(ERROR_STATUS, message, null); + } + + private APIResponse(String status, String message, T data) { + this.status = status; + this.message = message; + this.data = data; + } +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/config/CorsConfig.java b/src/main/java/com/capstone/favicon/config/CorsConfig.java index 18c44ab..f025d5c 100644 --- a/src/main/java/com/capstone/favicon/config/CorsConfig.java +++ b/src/main/java/com/capstone/favicon/config/CorsConfig.java @@ -18,6 +18,7 @@ public CorsConfigurationSource corsConfigurationSource() { configuration.addAllowedOrigin("http://127.0.0.1:3000"); configuration.addAllowedOrigin("http://localhost:3001"); configuration.addAllowedOrigin("http://127.0.0.1:3001"); + configuration.addAllowedOrigin("http://3.35.26.19"); configuration.addAllowedHeader("*"); configuration.addAllowedMethod("*"); diff --git a/src/main/java/com/capstone/favicon/config/RedisConfig.java b/src/main/java/com/capstone/favicon/config/RedisConfig.java new file mode 100644 index 0000000..3e9ce86 --- /dev/null +++ b/src/main/java/com/capstone/favicon/config/RedisConfig.java @@ -0,0 +1,41 @@ +package com.capstone.favicon.config; + + +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.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@RequiredArgsConstructor +@EnableRedisRepositories +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private String port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, Integer.parseInt(port)); + } + + @Bean + public RedisTemplate redisTemplate() { + + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory()); + + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + + return template; + } +} diff --git a/src/main/java/com/capstone/favicon/config/S3Config.java b/src/main/java/com/capstone/favicon/config/S3Config.java new file mode 100644 index 0000000..0e79434 --- /dev/null +++ b/src/main/java/com/capstone/favicon/config/S3Config.java @@ -0,0 +1,187 @@ +package com.capstone.favicon.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.*; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class S3Config { + protected final S3Client s3Client; + private final String region; + + @Value("${aws.s3.bucket-name}") + private String bucketName; + + public S3Config( + @Value("${aws.s3.region}") String region, + @Value("${aws.s3.access-key}") String accessKey, + @Value("${aws.s3.secret-key}") String secretKey) { + this.region = region; + this.s3Client = S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey))) + .build(); + } + + public String uploadFile(MultipartFile file, String directory) throws IOException { + String fileName = file.getOriginalFilename(); + String fullKey = directory + "/" + fileName; // e.g. preprocessing/파일명.csv + String encodedFileName = URLEncoder.encode(fullKey, StandardCharsets.UTF_8).replace("+", "%20"); + + PutObjectRequest putRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(fullKey) + .contentType(file.getContentType()) + .build(); + + s3Client.putObject(putRequest, RequestBody.fromBytes(file.getBytes())); + + return "s3://" + bucketName + "/" + fullKey; + } + + public void deleteFile(String fileUrl) { + String key = extractKeyFromUrl(fileUrl); + + DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(); + + s3Client.deleteObject(deleteRequest); + } + + public void deleteFileByKey(String key) { + DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(); + + s3Client.deleteObject(deleteRequest); + } + + public void moveFile(String fromKey, String toKey) { + CopyObjectRequest copyRequest = CopyObjectRequest.builder() + .sourceBucket(bucketName) + .sourceKey(fromKey) + .destinationBucket(bucketName) + .destinationKey(toKey) + .build(); + + s3Client.copyObject(copyRequest); + + DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder() + .bucket(bucketName) + .key(fromKey) + .build(); + + s3Client.deleteObject(deleteRequest); + } + + /*public List listFilesInBucket() { + ListObjectsV2Request listObjectsV2Request = ListObjectsV2Request.builder() + .bucket(bucketName) + .build(); + + ListObjectsV2Response response = s3Client.listObjectsV2(listObjectsV2Request); + + return response.contents().stream() + .map(object -> object.key()) + .collect(Collectors.toList()); + }*/ + public List listFilesInBucket() { + List allKeys = new ArrayList<>(); + String continuationToken = null; + + do { + ListObjectsV2Request.Builder requestBuilder = ListObjectsV2Request.builder() + .bucket(bucketName) + .maxKeys(1000); + + if (continuationToken != null) { + requestBuilder.continuationToken(continuationToken); + } + + ListObjectsV2Response response = s3Client.listObjectsV2(requestBuilder.build()); + + List keys = response.contents().stream() + .map(S3Object::key) + .collect(Collectors.toList()); + + allKeys.addAll(keys); + + continuationToken = response.nextContinuationToken(); + } while (continuationToken != null); + + return allKeys; + } + + + /** + * key에서 fileUrl 추출 + * @param fileUrl + */ + public String extractKeyFromUrl(String fileUrl) { + return fileUrl.substring(fileUrl.indexOf(bucketName) + bucketName.length() + 1); + } + + /** + * key에서 fileName 추출 + * @param key + */ + public String extractFileNameFromKey(String key) { + return key.substring(key.lastIndexOf("/")+1); + } + + protected String encodeFileName(String fileName) throws UnsupportedEncodingException { + return URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); + } + + public String generateFileUrl(String fileName) { + return "s3://" + bucketName + "/" + fileName; //다운로드 기능 테스트 및 경로 물어보기 + //return "https://favicon-dataset.s3.ap-northeast-2.amazonaws.com/" + fileName; + } + + public LocalDate getLastModifiedDate(String s3Key) { + HeadObjectRequest headRequest = HeadObjectRequest.builder() + .bucket(bucketName) + .key(s3Key) + .build(); + + HeadObjectResponse headObjectResponse = s3Client.headObject(headRequest); + + return headObjectResponse.lastModified() + .atZone(ZoneId.systemDefault()) + .toLocalDate(); + } + + public String extractKeyFromAnyUrl(String fileUrl) { + if (fileUrl.startsWith("s3://")) { + int bucketNameEnd = fileUrl.indexOf("/", 5); // after "s3://" + if (bucketNameEnd == -1) { + throw new IllegalArgumentException("Invalid s3 URL: " + fileUrl); + } + return fileUrl.substring(bucketNameEnd + 1); + } else if (fileUrl.contains(".amazonaws.com/")) { + return fileUrl.substring(fileUrl.indexOf(".com/") + 5); + } else { + return extractKeyFromUrl(fileUrl); + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/config/SecurityConfig.java b/src/main/java/com/capstone/favicon/config/SecurityConfig.java new file mode 100644 index 0000000..4de4d8a --- /dev/null +++ b/src/main/java/com/capstone/favicon/config/SecurityConfig.java @@ -0,0 +1,43 @@ +package com.capstone.favicon.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .cors(Customizer.withDefaults()) + .csrf((csrfConfig) -> + csrfConfig.disable() + ) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/users/**", "/statistics/**", "/admin/**", "/gpt/chat", + "/notice/create", "/notice/list", "/notice/{noticeId}", "/notice/view/{noticeId}", "/faq/create", "/faq/{faqId}", + "/data-set/filter", "/data-set/count","/data-set/ratio", "/data-set/incrementDownload/{datasetId}", "/data-set/top9", + "/data-set/theme", "/data-set/{datasetId}", "/data-set/category/{themeId}", "/data-set/filter", "/faq/list", "/faq/{faqId}", + "/s3/upload", "/s3/delete/{resourceId}", "/analysis", "/data-set/stats", "/request/stats", "/request/download/{requestId}", + "/data-set", "/request/list","/request/list/{requestId}/review", "/request/{requestId}","/request/question", + "/request/question/{questionId}", "/request/answer", "/request/answer/{answerId}","/trend/**", "/data-set/search-sorted", "/data-set/search-sorted/{category}", + "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", "/data-set/download/{datasetId}", "/data-set/group-by-theme", "/region").permitAll() + .anyRequest().authenticated() + ) + .httpBasic(httpBasic -> httpBasic.disable()) + .formLogin(formLogin -> formLogin.disable()) + .sessionManagement(session -> session + .sessionFixation().migrateSession() + .maximumSessions(1) + ) + .addFilterBefore(new Utf8Filter(), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} diff --git a/src/main/java/com/capstone/favicon/config/Utf8Filter.java b/src/main/java/com/capstone/favicon/config/Utf8Filter.java new file mode 100644 index 0000000..e00a4f3 --- /dev/null +++ b/src/main/java/com/capstone/favicon/config/Utf8Filter.java @@ -0,0 +1,19 @@ +package com.capstone.favicon.config; + +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import jakarta.servlet.*; + +import java.io.IOException; + +@Component +public class Utf8Filter implements Filter{ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletResponse res = (HttpServletResponse) response; + res.setCharacterEncoding("UTF-8"); + res.setContentType("text/html; charset=UTF-8"); + chain.doFilter(request, response); + } + +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/AnalysisServiceImpl.java b/src/main/java/com/capstone/favicon/dataset/application/AnalysisServiceImpl.java new file mode 100644 index 0000000..a52b727 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/AnalysisServiceImpl.java @@ -0,0 +1,107 @@ +package com.capstone.favicon.dataset.application; + +import com.capstone.favicon.dataset.application.service.AnalysisService; +import com.capstone.favicon.dataset.dto.AnalysisRequestDto; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Setter +@Service +public class AnalysisServiceImpl implements AnalysisService { + private final ObjectMapper objectMapper; + + public AnalysisServiceImpl(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public Map analyze(AnalysisRequestDto requestDto) { + try { + + String pythonCmd = "venv/bin/python3"; + + // Python 스크립트 경로 + String scriptPath = Paths.get("analysis.py").toAbsolutePath().toString(); + + + // ProcessBuilder 설정 + ProcessBuilder pb = new ProcessBuilder( + pythonCmd, scriptPath, + requestDto.getTheme1(), + requestDto.getTheme2(), + requestDto.getRegion(), + requestDto.getStart().toString(), + requestDto.getEnd().toString() + ); + + pb.redirectErrorStream(true); + + Process process = pb.start(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + StringBuilder output = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + output.append(line); + } + + BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + StringBuilder errorOutput = new StringBuilder(); + while ((line = errorReader.readLine()) != null) { + errorOutput.append(line).append("\n"); + } + + + int exitCode = process.waitFor(); + if (exitCode != 0) { + System.err.println("[Python Error Output]"); + System.err.println(errorOutput); + System.err.println("Standard Output:"); + System.err.println(output); + log.error("Python process exited with code {}", exitCode); + log.error("Python Output: {}", output.toString()); + throw new RuntimeException("Python process failed."); + } + + // 결과가 JSON인지 확인 + Map result = new HashMap<>(); + try { + String fullOutput = output.toString().trim(); + int jsonStart = fullOutput.indexOf('{'); + + if (jsonStart != -1) { + String jsonPart = fullOutput.substring(jsonStart); + try { + result = objectMapper.readValue(jsonPart, new TypeReference<>() {}); + } catch (Exception parseEx) { + log.warn("유효 JSON 파싱 실패: {}", jsonPart); + result.put("message", "JSON 파싱 실패"); + } + } else { + log.warn("JSON 시작 구분자 '{' 없음: {}", fullOutput); + result.put("message", "유효한 JSON 없음"); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + return result; + + + } catch (Exception e) { + log.error("파이썬 분석 중 에러 발생", e); + throw new RuntimeException("분석 실패", e); + } + } + +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/DatasetServiceImpl.java b/src/main/java/com/capstone/favicon/dataset/application/DatasetServiceImpl.java new file mode 100644 index 0000000..623ba9e --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/DatasetServiceImpl.java @@ -0,0 +1,161 @@ +package com.capstone.favicon.dataset.application; + +import com.capstone.favicon.dataset.application.service.DatasetService; +import com.capstone.favicon.dataset.domain.Dataset; +import com.capstone.favicon.dataset.domain.DatasetTheme; +import com.capstone.favicon.dataset.repository.DatasetRepository; +import com.capstone.favicon.dataset.repository.DatasetThemeRepository; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.YearMonth; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.TreeMap; + +@Service +public class DatasetServiceImpl implements DatasetService { + + private final DatasetRepository datasetRepository; + + private DatasetThemeRepository datasetThemeRepository; + + @Autowired + public DatasetServiceImpl(DatasetRepository datasetRepository) { + this.datasetRepository = datasetRepository; + } + + @Override + public List findAllDatasets() { + return datasetRepository.findAll(); + } + + @Override + public List getTop9ByDownload() { + return datasetRepository.findTop9ByOrderByDownloadDesc(); + } + + @Transactional + @Override + public void incrementDownloadCount(Long datasetId) { + Dataset dataset = datasetRepository.findById(datasetId).orElseThrow(() -> new RuntimeException("Dataset not found with id: " + datasetId)); + dataset.setDownload(dataset.getDownload() + 1); + datasetRepository.save(dataset); + } + + @Override + public List filterByCategory(String theme) { + return datasetThemeRepository.findByTheme(theme); + } + + @Override + public long getTotalDatasetCount() { + return datasetRepository.count(); + } + + @Override + public Optional getDatasetDetails(Long datasetId) { + return datasetRepository.findById(datasetId); + } + + @Override + public Map> getThemeStats() { + long total = datasetRepository.count(); + + if (total == 0) { + return Map.of( + "기후", Map.of("count", 0, "ratio", 0), + "환경", Map.of("count", 0, "ratio", 0), + "질병", Map.of("count", 0, "ratio", 0) + ); + } + + long climateCount = datasetRepository.countByDatasetTheme_DatasetThemeId(1L); + long environmentCount = datasetRepository.countByDatasetTheme_DatasetThemeId(2L); + long diseaseCount = datasetRepository.countByDatasetTheme_DatasetThemeId(3L); + + int climateRatio = (int) Math.round((double) climateCount / total * 100); + int environmentRatio = (int) Math.round((double) environmentCount / total * 100); + int diseaseRatio = (int) Math.round((double) diseaseCount / total * 100); + + return Map.of( + "기후", Map.of("count", climateCount, "ratio", climateRatio), + "환경", Map.of("count", environmentCount, "ratio", environmentRatio), + "질병", Map.of("count", diseaseCount, "ratio", diseaseRatio) + ); + } + + @Override + public List getDatasetsByCategory(Long datasetThemeId) { + return datasetRepository.findByDatasetTheme_DatasetThemeId(datasetThemeId); + } + + @Override + public List search(String text) { + return datasetRepository.searchByText(text); + } + +// public List searchWithCategory(String text, String category) { +// return datasetRepository.searchWithCategory(text, category); +// } + + /*** + * theme(질병, 기후, 환경) 별 세부카테고리(감기, 미세먼지, 기온 등) 목록 조회 + */ + @Override + public Map> getDatasetNameGroupByTheme() { + List dataset = datasetRepository.findAllWithTheme(); + + return dataset.stream() + .filter(d -> d.getDatasetTheme() != null && d.getDatasetTheme().getTheme() != null) + .collect(Collectors.groupingBy( + d -> d.getDatasetTheme().getTheme(), + Collectors.mapping(Dataset::getName, Collectors.toList()) + )); + } + + @Override + public Map> getMonthlyDatasetStats() { + List datasets = datasetRepository.findAll(); + + if (datasets.isEmpty()) return Map.of(); + + Map monthlyCounts = datasets.stream() + .filter(d -> d.getCreatedDate() != null) + .collect(Collectors.groupingBy( + d -> YearMonth.from(d.getCreatedDate()), + TreeMap::new, + Collectors.counting() + )); + + YearMonth now = YearMonth.now(); + List last6Months = IntStream.rangeClosed(0, 5) + .mapToObj(i -> now.minusMonths(5 - i)) + .toList(); + + Map> result = new LinkedHashMap<>(); + long cumulative = 0; + long prev = 0; + + for (YearMonth ym : last6Months) { + long count = monthlyCounts.getOrDefault(ym, 0L); + cumulative += count; + int growthRate = (prev > 0) ? (int) Math.round(((double)(cumulative - prev) / prev) * 100) : 0; + + result.put(ym.toString(), Map.of( + "개수", cumulative, + "증가율", growthRate + )); + + prev = cumulative; + } + + return result; + } + +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/dataset/application/DatasetThemeServiceImpl.java b/src/main/java/com/capstone/favicon/dataset/application/DatasetThemeServiceImpl.java new file mode 100644 index 0000000..66c43ab --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/DatasetThemeServiceImpl.java @@ -0,0 +1,78 @@ +package com.capstone.favicon.dataset.application; + +import com.capstone.favicon.dataset.application.service.DatasetThemeService; +import com.capstone.favicon.dataset.domain.DatasetTheme; +import com.capstone.favicon.dataset.repository.DatasetThemeRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class DatasetThemeServiceImpl implements DatasetThemeService { + + private final DatasetThemeRepository datasetThemeRepository; + + @Autowired + public DatasetThemeServiceImpl(DatasetThemeRepository datasetThemeRepository) { + this.datasetThemeRepository = datasetThemeRepository; + } + + @Override + public List getDatasets (String region, Integer dataYear, String fileType) { + if (region != null && dataYear != null && fileType != null) { + return getFilteredDatasets(region, dataYear, fileType); + } else if (region != null && dataYear != null) { + return getDatasetsByRegionAndDataYear(region, dataYear); + } else if (region != null && fileType != null) { + return getDatasetsByRegionAndFileType(region, fileType); + } else if (dataYear != null && fileType != null) { + return getDatasetsByDataYearAndFileType(dataYear, fileType); + } else if (region != null) { + return getDatasetsByRegion(region); + } else if (dataYear != null) { + return getDatasetsByDataYear(dataYear); + } else if (fileType != null) { + return getDatasetsByFileType(fileType); + } else { + return getAllDatasets(); + } + } + + private List getFilteredDatasets(String region, Integer dataYear, String fileType) { + return datasetThemeRepository.findByRegionAndDataYearAndFileType(region, dataYear, fileType); + } + + private List getDatasetsByRegionAndDataYear(String region, Integer dataYear) { + return datasetThemeRepository.findByRegionAndDataYear(region, dataYear); + } + + private List getDatasetsByRegionAndFileType(String region, String fileType) { + return datasetThemeRepository.findByRegionAndFileType(region, fileType); + } + + private List getDatasetsByDataYearAndFileType(Integer dataYear, String fileType) { + return datasetThemeRepository.findByDataYearAndFileType(dataYear, fileType); + } + + private List getDatasetsByRegion(String region) { + return datasetThemeRepository.findByRegion(region); + } + + private List getDatasetsByDataYear(Integer dataYear) { + return datasetThemeRepository.findByDataYear(dataYear); + } + + private List getDatasetsByFileType(String fileType) { + return datasetThemeRepository.findByFileType(fileType); + } + + private List getAllDatasets() { + return datasetThemeRepository.findAll(); + } + + private long getTotalDatasetCount() { + return datasetThemeRepository.count(); + } + +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/FilePathServiceImpl.java b/src/main/java/com/capstone/favicon/dataset/application/FilePathServiceImpl.java new file mode 100644 index 0000000..1ad5c22 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/FilePathServiceImpl.java @@ -0,0 +1,20 @@ +package com.capstone.favicon.dataset.application; + +import com.capstone.favicon.dataset.application.service.FilePathService; +import org.springframework.stereotype.Service; + +import java.nio.file.Paths; + +@Service +public class FilePathServiceImpl implements FilePathService { + + /** + * 다운로드 디렉토리 경로 가져오기 + * @return 다운로드 디렉토리 경로 + */ + @Override + public String getDownloadDir() { + String userHome = System.getProperty("user.home"); + return Paths.get(userHome, "Downloads").toString(); + } +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/OpenAIServiceImpl.java b/src/main/java/com/capstone/favicon/dataset/application/OpenAIServiceImpl.java new file mode 100644 index 0000000..8e71bb3 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/OpenAIServiceImpl.java @@ -0,0 +1,38 @@ +package com.capstone.favicon.dataset.application; + +import com.capstone.favicon.dataset.application.service.OpenAIService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class OpenAIServiceImpl implements OpenAIService { + + @Value("${openai.api-key}") + private String apiKey; + + @Override + public String chat(List> messages) { + WebClient client = WebClient.builder() + .baseUrl("https://api.openai.com/v1/chat/completions") + .defaultHeader("Authorization", "Bearer " + apiKey).build(); + Map body = Map.of( + "model", "gpt-3.5-turbo", + "messages", messages, + "temperature", 0.2, + "max_tokens", 400 + ); + + return client.post() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(body) + .retrieve() + .bodyToMono(String.class) + .block(); + } +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/RegionServiceImpl.java b/src/main/java/com/capstone/favicon/dataset/application/RegionServiceImpl.java new file mode 100644 index 0000000..45adaee --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/RegionServiceImpl.java @@ -0,0 +1,20 @@ +package com.capstone.favicon.dataset.application; + +import com.capstone.favicon.dataset.application.service.RegionService; +import com.capstone.favicon.dataset.repository.RegionRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class RegionServiceImpl implements RegionService { + + @Autowired + private RegionRepository regionRepository; + + @Override + public List findAllRegionNames() { + return regionRepository.findAllRegionNames(); + } +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/ResourceServiceImpl.java b/src/main/java/com/capstone/favicon/dataset/application/ResourceServiceImpl.java new file mode 100644 index 0000000..901c935 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/ResourceServiceImpl.java @@ -0,0 +1,47 @@ +package com.capstone.favicon.dataset.application; + +import com.capstone.favicon.dataset.application.service.ResourceService; +import com.capstone.favicon.dataset.domain.FileExtension; +import com.capstone.favicon.dataset.domain.Resource; +import com.capstone.favicon.dataset.repository.ResourceRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@RequiredArgsConstructor +@Service +@Slf4j +public class ResourceServiceImpl implements ResourceService { + + @Autowired + private ResourceRepository resourceRepository; + + /** + * datasetID로 resourceUrl 가져오기 + * @param datasetId + * @return resourceUrl + */ + @Override + public String getResourceUrlByDatasetId(Long datasetId) { + Resource resource = resourceRepository.findByDatasetDatasetId(datasetId) + .orElseThrow(() -> { + log.error("Resource Not Found for Dataset ID: {}", datasetId); + return new RuntimeException("Resource Not Found by DatasetID " + datasetId); + }); + return resource.getResourceUrl(); + } + + /** + * datasetID로 fileExtension 가져오기 + * @param datasetId + * @return FileExtension + */ + @Override + public FileExtension getFileExtensionByDatasetId(Long datasetId) { + Optional resource = resourceRepository.findByDatasetDatasetId(datasetId); + return resource.map(Resource::getType).orElse(FileExtension.TXT); + } +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/dataset/application/S3FileDownloadServiceImpl.java b/src/main/java/com/capstone/favicon/dataset/application/S3FileDownloadServiceImpl.java new file mode 100644 index 0000000..2b35aed --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/S3FileDownloadServiceImpl.java @@ -0,0 +1,127 @@ +package com.capstone.favicon.dataset.application; + +import com.capstone.favicon.config.S3Config; +import com.capstone.favicon.dataset.application.service.FilePathService; +import com.capstone.favicon.dataset.application.service.ResourceService; +import com.capstone.favicon.dataset.application.service.S3FileDownloadService; +import com.capstone.favicon.dataset.domain.FileExtension; +import com.capstone.favicon.user.application.service.RequestService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +@Service +public class S3FileDownloadServiceImpl extends S3Config implements S3FileDownloadService { + @Autowired + private ResourceService resourceService; + @Autowired + private FilePathService filePathService; + @Autowired + private RequestService requestService; + + @Value("${aws.s3.bucket-name}") + private String bucketName; + + public S3FileDownloadServiceImpl(@Value("${aws.s3.region}") String region, + @Value("${aws.s3.access-key}") String accessKey, + @Value("${aws.s3.secret-key}") String secretKey) { + super(region, accessKey, secretKey); + } + + /** + * S3에서 가져온 파일을 사용자의 다운로드 폴더에 저장 + * @param datasetId 데이터셋 ID + * @return 다운로드된 파일 + * @throws IOException 파일 다운로드 중 발생할 수 있는 예외 + */ + @Override + public File downloadFile(Long datasetId) throws IOException { + + // 다운로드 경로 설정 + String downloadDir = filePathService.getDownloadDir(); + + // 데이터셋 ID를 기반으로 파일 URL, 확장자 가져오기 + String fileUrl = resourceService.getResourceUrlByDatasetId(datasetId); + FileExtension fileExtension = resourceService.getFileExtensionByDatasetId(datasetId); + + // S3에 저장된 파일 키, 이름 + String key = extractKeyFromUrl(fileUrl); + String fileName = extractFileNameFromKey(key); + String encodedFileName = encodeFileName(fileName); + File file = createFileWithExtension(downloadDir, encodedFileName, fileExtension); + + // file에 내용 써서 저장 + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(); + + try (ResponseInputStream s3Object = s3Client.getObject(getObjectRequest); + FileOutputStream fos = new FileOutputStream(file)) { + byte[] buffer = new byte[1024]; + int length; + while ((length = s3Object.read(buffer)) != -1) { + fos.write(buffer, 0, length); + } + } + + return file; + } + + /** + * 사용자의 다운로드 폴더에 지정된 확장자로 파일을 생성 + * @param directory 다운로드 경로 + * @param fileName 파일명 + * @param extension 파일 확장자 + * @return 생성된 파일 + * @throws IOException 파일 생성 중 발생할 수 있는 예외 + */ + private File createFileWithExtension(String directory, String fileName, FileExtension extension) throws IOException { + File downloadDir = new File(directory); + if (!downloadDir.exists()) { + downloadDir.mkdirs(); // 폴더가 없으면 생성 + } + // String encodedFileName = encodeFileName(fileName); + return new File(downloadDir, fileName + "." + extension.name().toLowerCase()); + } + + + + public File downloadFileFromDataRequest(Long dataRequestId) throws IOException { + String downloadDir = filePathService.getDownloadDir(); + + String fileUrl = requestService.getFileUrlByRequestId(dataRequestId); + FileExtension fileExtension = requestService.getFileExtensionByRequestId(dataRequestId); + + String key = extractKeyFromUrl(fileUrl); + String fileName = extractFileNameFromKey(key); + String encodedFileName = encodeFileName(fileName); + File file = createFileWithExtension(downloadDir, encodedFileName, fileExtension); + + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(); + + try (ResponseInputStream s3Object = s3Client.getObject(getObjectRequest); + FileOutputStream fos = new FileOutputStream(file)) { + byte[] buffer = new byte[1024]; + int length; + while ((length = s3Object.read(buffer)) != -1) { + fos.write(buffer, 0, length); + } + } + + return file; + } + + + +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/dataset/application/TrendSchedulerServiceImpl.java b/src/main/java/com/capstone/favicon/dataset/application/TrendSchedulerServiceImpl.java new file mode 100644 index 0000000..d20ad3b --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/TrendSchedulerServiceImpl.java @@ -0,0 +1,56 @@ +package com.capstone.favicon.dataset.application; + +import com.capstone.favicon.dataset.application.service.TrendSchedulerService; +import com.capstone.favicon.dataset.domain.Dataset; +import com.capstone.favicon.dataset.domain.Trend; +import com.capstone.favicon.dataset.repository.DatasetRepository; +import com.capstone.favicon.dataset.repository.TrendRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class TrendSchedulerServiceImpl implements TrendSchedulerService { + + private final DatasetRepository datasetRepository; + private final TrendRepository trendRepository; + + /*@Scheduled(cron = "0 * * * * *") */ // 매 1분마다 실행(테스트용으로 하기) + @Scheduled(cron = "0 0 0 * * *") // 매일 자정(실제 배포용으로) + @Override + public void analyzeTrends() { + List datasets = datasetRepository.findAllByOrderByDownloadDesc(); + LocalDate today = LocalDate.now(); + LocalDate yesterday = today.minusDays(1); + + for (int i = 0; i < datasets.size(); i++) { + Dataset dataset = datasets.get(i); + int currentRank = i + 1; + + Optional previousTrendOpt = trendRepository.findByDatasetIdAndDate(dataset.getDatasetId(), yesterday); + + String status = "유지"; + if (previousTrendOpt.isPresent()) { + int previousRank = previousTrendOpt.get().getRank(); + if (currentRank < previousRank) { + status = "상승"; + } else if (currentRank > previousRank) { + status = "하락"; + } + } + + Trend trend = new Trend(); + trend.setDataset(dataset); + trend.setRank(currentRank); + trend.setTrendStatus(status); + trend.setRankDate(today); + + trendRepository.save(trend); + } + } +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/service/AnalysisService.java b/src/main/java/com/capstone/favicon/dataset/application/service/AnalysisService.java new file mode 100644 index 0000000..46ce907 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/service/AnalysisService.java @@ -0,0 +1,9 @@ +package com.capstone.favicon.dataset.application.service; + +import com.capstone.favicon.dataset.dto.AnalysisRequestDto; + +import java.util.Map; + +public interface AnalysisService { + Map analyze(AnalysisRequestDto requestDto); +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/service/DatasetService.java b/src/main/java/com/capstone/favicon/dataset/application/service/DatasetService.java new file mode 100644 index 0000000..d755f14 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/service/DatasetService.java @@ -0,0 +1,34 @@ +package com.capstone.favicon.dataset.application.service; + +import com.capstone.favicon.dataset.domain.Dataset; +import com.capstone.favicon.dataset.domain.DatasetTheme; +import jakarta.transaction.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public interface DatasetService { + List findAllDatasets(); + + List getTop9ByDownload(); + + @Transactional + void incrementDownloadCount(Long datasetId); + + List filterByCategory(String theme); + + long getTotalDatasetCount(); + + Optional getDatasetDetails(Long datasetId); + + Map> getThemeStats(); + + List getDatasetsByCategory(Long datasetThemeId); + + List search(String text); + + Map> getDatasetNameGroupByTheme(); + + Map> getMonthlyDatasetStats(); +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/service/DatasetThemeService.java b/src/main/java/com/capstone/favicon/dataset/application/service/DatasetThemeService.java new file mode 100644 index 0000000..8a48fc6 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/service/DatasetThemeService.java @@ -0,0 +1,9 @@ +package com.capstone.favicon.dataset.application.service; + +import com.capstone.favicon.dataset.domain.DatasetTheme; + +import java.util.List; + +public interface DatasetThemeService { + List getDatasets(String region, Integer dataYear, String fileType); +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/service/FilePathService.java b/src/main/java/com/capstone/favicon/dataset/application/service/FilePathService.java new file mode 100644 index 0000000..aec2877 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/service/FilePathService.java @@ -0,0 +1,5 @@ +package com.capstone.favicon.dataset.application.service; + +public interface FilePathService { + String getDownloadDir(); +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/service/OpenAIService.java b/src/main/java/com/capstone/favicon/dataset/application/service/OpenAIService.java new file mode 100644 index 0000000..2e6fd6c --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/service/OpenAIService.java @@ -0,0 +1,8 @@ +package com.capstone.favicon.dataset.application.service; + +import java.util.List; +import java.util.Map; + +public interface OpenAIService { + String chat(List> message); +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/service/RegionService.java b/src/main/java/com/capstone/favicon/dataset/application/service/RegionService.java new file mode 100644 index 0000000..42a079d --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/service/RegionService.java @@ -0,0 +1,10 @@ +package com.capstone.favicon.dataset.application.service; + +import com.capstone.favicon.dataset.domain.Region; +import com.capstone.favicon.dataset.repository.RegionRepository; + +import java.util.List; + +public interface RegionService { + List findAllRegionNames(); +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/service/ResourceService.java b/src/main/java/com/capstone/favicon/dataset/application/service/ResourceService.java new file mode 100644 index 0000000..da5afde --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/service/ResourceService.java @@ -0,0 +1,9 @@ +package com.capstone.favicon.dataset.application.service; + +import com.capstone.favicon.dataset.domain.FileExtension; + +public interface ResourceService { + String getResourceUrlByDatasetId(Long datasetId); + + FileExtension getFileExtensionByDatasetId(Long datasetId); +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/service/S3FileDownloadService.java b/src/main/java/com/capstone/favicon/dataset/application/service/S3FileDownloadService.java new file mode 100644 index 0000000..7d69999 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/service/S3FileDownloadService.java @@ -0,0 +1,10 @@ +package com.capstone.favicon.dataset.application.service; + +import java.io.File; +import java.io.IOException; + +public interface S3FileDownloadService { + File downloadFile(Long datasetId) throws IOException; + + File downloadFileFromDataRequest(Long dataRequestId) throws IOException; +} diff --git a/src/main/java/com/capstone/favicon/dataset/application/service/TrendSchedulerService.java b/src/main/java/com/capstone/favicon/dataset/application/service/TrendSchedulerService.java new file mode 100644 index 0000000..afc0a7a --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/application/service/TrendSchedulerService.java @@ -0,0 +1,9 @@ +package com.capstone.favicon.dataset.application.service; + +import org.springframework.scheduling.annotation.Scheduled; + +public interface TrendSchedulerService { + /*@Scheduled(cron = "0 * * * * *") */ // 매 1분마다 실행(테스트용으로 하기) + @Scheduled(cron = "0 0 0 * * *") // 매일 자정(실제 배포용으로) + void analyzeTrends(); +} diff --git a/src/main/java/com/capstone/favicon/dataset/controller/AnalysisController.java b/src/main/java/com/capstone/favicon/dataset/controller/AnalysisController.java new file mode 100644 index 0000000..409c16f --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/controller/AnalysisController.java @@ -0,0 +1,31 @@ +package com.capstone.favicon.dataset.controller; + +import com.capstone.favicon.config.APIResponse; +import com.capstone.favicon.dataset.application.service.AnalysisService; +import com.capstone.favicon.dataset.dto.AnalysisRequestDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@RequestMapping("/analysis") +public class AnalysisController { + + @Autowired + private AnalysisService analysisService; + + @PostMapping + public ResponseEntity> runAnalysis(@RequestBody AnalysisRequestDto requestDto) { + try { + Map result = analysisService.analyze(requestDto); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", result)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } +} diff --git a/src/main/java/com/capstone/favicon/dataset/controller/DatasetController.java b/src/main/java/com/capstone/favicon/dataset/controller/DatasetController.java new file mode 100644 index 0000000..0493042 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/controller/DatasetController.java @@ -0,0 +1,148 @@ +package com.capstone.favicon.dataset.controller; + +import com.capstone.favicon.config.APIResponse; +import com.capstone.favicon.dataset.application.service.DatasetService; +import com.capstone.favicon.dataset.domain.Dataset; +import com.capstone.favicon.dataset.domain.DatasetTheme; +import org.springframework.http.ResponseEntity; +import com.capstone.favicon.dataset.dto.SearchDto; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@RestController +@RequestMapping("/data-set") +public class DatasetController { + + private final DatasetService datasetService; + + public DatasetController(DatasetService datasetService) { + this.datasetService = datasetService; + } + + @GetMapping + public ResponseEntity> getDatasets() { + try { + List datasets = datasetService.findAllDatasets(); + return ResponseEntity.ok().body(APIResponse.successAPI("success", datasets)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/top9") + public ResponseEntity> getTop9Datasets() { + try { + List datasets = datasetService.getTop9ByDownload(); + return ResponseEntity.ok().body(APIResponse.successAPI("success", datasets)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @PostMapping("/incrementDownload/{datasetId}") + public ResponseEntity> incrementDownload(@PathVariable Long datasetId) { + try { + datasetService.incrementDownloadCount(datasetId); + return ResponseEntity.ok().body(APIResponse.successAPI("success", null)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/theme") + public ResponseEntity> filterByCategory(@RequestParam String theme) { + try { + List datasetThemes = datasetService.filterByCategory(theme); + return ResponseEntity.ok().body(APIResponse.successAPI("success", datasetThemes)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/count") + public ResponseEntity> getTotalDatasetCount() { + try { + long count = datasetService.getTotalDatasetCount(); + return ResponseEntity.ok().body(APIResponse.successAPI("success", count)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/{datasetId:\\d+}") + public ResponseEntity> getDatasetDetails(@PathVariable Long datasetId) { + try { + Optional dataset = datasetService.getDatasetDetails(datasetId); + return ResponseEntity.ok().body(APIResponse.successAPI("success", dataset)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/ratio") + public ResponseEntity> getThemeStats() { + try { + Map> themeStats = datasetService.getThemeStats(); + return ResponseEntity.ok().body(APIResponse.successAPI("success", themeStats)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/category/{themeId}") + public ResponseEntity> getDatasetsByCategory(@PathVariable Long themeId) { + try { + List datasets = datasetService.getDatasetsByCategory(themeId); + return ResponseEntity.ok().body(APIResponse.successAPI("success", datasets)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/search-sorted") + public ResponseEntity> search(@RequestBody SearchDto searchDto) { + try { + List dataList = datasetService.search(searchDto.getText()); + return ResponseEntity.ok().body(APIResponse.successAPI("검색결과", dataList)); + } catch (Exception e) { + String message = e.getMessage(); + return ResponseEntity.badRequest().body(APIResponse.errorAPI(message)); + } + } + + @GetMapping("/group-by-theme") + public ResponseEntity> getDatasetsGroupedByTheme() { + try { + Map> result = datasetService.getDatasetNameGroupByTheme(); + return ResponseEntity.ok().body(APIResponse.successAPI("success", result)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/stats") + public ResponseEntity> getMonthlyStats() { + try { + Map> stats = datasetService.getMonthlyDatasetStats(); + return ResponseEntity.ok().body(APIResponse.successAPI("success", stats)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + +// @GetMapping("/search-sorted/{category}") +// public ResponseEntity> searchWithCategory(@PathVariable("category") String category, @RequestBody SearchDto searchDto) { +// try { +// List dataList = datasetService.searchWithCategory(searchDto.getText(), category); +// return ResponseEntity.ok().body(APIResponse.successAPI("검색결과", dataList)); +// } catch (Exception e) { +// String message = e.getMessage(); +// return ResponseEntity.badRequest().body(APIResponse.errorAPI(message)); +// } +// } + +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/dataset/controller/DatasetThemeController.java b/src/main/java/com/capstone/favicon/dataset/controller/DatasetThemeController.java new file mode 100644 index 0000000..eaec248 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/controller/DatasetThemeController.java @@ -0,0 +1,40 @@ +package com.capstone.favicon.dataset.controller; + +import com.capstone.favicon.config.APIResponse; +import com.capstone.favicon.dataset.application.service.DatasetThemeService; +import com.capstone.favicon.dataset.domain.DatasetTheme; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/data-set") +public class DatasetThemeController { + + private final DatasetThemeService datasetThemeService; + + @Autowired + public DatasetThemeController(DatasetThemeService datasetThemeService) { + this.datasetThemeService = datasetThemeService; + } + + @GetMapping("/filter") + public ResponseEntity> getDatasets( + @RequestParam(name = "region", required = false) String region, + @RequestParam(name = "dataYear", required = false) Integer dataYear, + @RequestParam(name = "fileType", required = false) String fileType) { + + try { + List datasets = datasetThemeService.getDatasets(region, dataYear, fileType); + return ResponseEntity.ok().body(APIResponse.successAPI("success", datasets)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + +} diff --git a/src/main/java/com/capstone/favicon/dataset/controller/GPTController.java b/src/main/java/com/capstone/favicon/dataset/controller/GPTController.java new file mode 100644 index 0000000..5fb56e6 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/controller/GPTController.java @@ -0,0 +1,34 @@ +package com.capstone.favicon.dataset.controller; + +import com.capstone.favicon.config.APIResponse; +import com.capstone.favicon.dataset.application.service.OpenAIService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/gpt") +public class GPTController { + + private final OpenAIService openAIService; + + public GPTController(OpenAIService openAIService) { + this.openAIService = openAIService; + } + + @PostMapping("/chat") + public ResponseEntity> chat(@RequestBody Map request) { + List> messages = (List>) request.get("messages"); + try { + String response = openAIService.chat(messages); + return ResponseEntity.ok().body(APIResponse.successAPI("success",response)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } +} diff --git a/src/main/java/com/capstone/favicon/dataset/controller/RegionController.java b/src/main/java/com/capstone/favicon/dataset/controller/RegionController.java new file mode 100644 index 0000000..5925eff --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/controller/RegionController.java @@ -0,0 +1,30 @@ +package com.capstone.favicon.dataset.controller; + + +import com.capstone.favicon.config.APIResponse; +import com.capstone.favicon.dataset.application.service.RegionService; +import org.springframework.beans.factory.annotation.Autowired; +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("/region") +public class RegionController { + + @Autowired + private RegionService regionService; + + @GetMapping + public ResponseEntity> getAllRegions() { + try { + List regions = regionService.findAllRegionNames(); + return ResponseEntity.ok().body(APIResponse.successAPI("success", regions)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } +} diff --git a/src/main/java/com/capstone/favicon/dataset/controller/TrendController.java b/src/main/java/com/capstone/favicon/dataset/controller/TrendController.java new file mode 100644 index 0000000..ef205d5 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/controller/TrendController.java @@ -0,0 +1,78 @@ +package com.capstone.favicon.dataset.controller; + +import com.capstone.favicon.config.APIResponse; +import com.capstone.favicon.dataset.domain.Dataset; +import com.capstone.favicon.dataset.domain.Trend; +import com.capstone.favicon.dataset.repository.DatasetRepository; +import com.capstone.favicon.dataset.repository.TrendRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("/trend") +@RequiredArgsConstructor +public class TrendController { + + private final TrendRepository trendRepository; + private final DatasetRepository datasetRepository; + + // 트렌드 데이터 확인(당일 기준으로 조회 하면 됨) + @GetMapping("/daily") + public ResponseEntity> getTrendsByDate(@RequestParam("date") LocalDate date) { + try { + List trends = trendRepository.findAllByRankDate(date); + if (trends.isEmpty()) { + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(APIResponse.errorAPI("찾을 수 없음")); + } + return ResponseEntity.ok().body(APIResponse.successAPI("success", trends)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + // 특정 Dataset의 트렌드 확인용 + @GetMapping("/{datasetId}") + public ResponseEntity> getTrendsByDatasetId( + @PathVariable Long datasetId, + @RequestParam("startDate") LocalDate startDate, + @RequestParam("endDate") LocalDate endDate) { + + try { + List trends = trendRepository.findByDatasetIdAndDateRange(datasetId, startDate, endDate); + if (trends.isEmpty()) { + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(APIResponse.errorAPI("찾을 수 없음")); + } + return ResponseEntity.ok().body(APIResponse.successAPI("success", trends)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + // 특정 Dataset의 현재 순위 조회 + @GetMapping("/rank/{datasetId}") + public ResponseEntity> getCurrentRank(@PathVariable Long datasetId) { + List datasets = datasetRepository.findAllByOrderByDownloadDesc(); + try { + for (int i = 0; i < datasets.size(); i++) { + if (datasets.get(i).getDatasetId().equals(datasetId)) { + return ResponseEntity.ok().body(APIResponse.successAPI("순위 조회 성공", i+1)); // 순위는 1부터 시작 + } + } + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(APIResponse.errorAPI("찾을 수 없음")); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + +} diff --git a/src/main/java/com/capstone/favicon/dataset/controller/s3FileDownloadController.java b/src/main/java/com/capstone/favicon/dataset/controller/s3FileDownloadController.java new file mode 100644 index 0000000..219d872 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/controller/s3FileDownloadController.java @@ -0,0 +1,37 @@ +package com.capstone.favicon.dataset.controller; + +import com.capstone.favicon.config.APIResponse; +import com.capstone.favicon.dataset.application.service.S3FileDownloadService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.FileSystemResource; +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.*; + + +import java.io.File; +import java.io.IOException; +import java.net.URLEncoder; + +@RestController +@RequestMapping("/data-set") +public class s3FileDownloadController { + + @Autowired + private S3FileDownloadService s3FileDownloadService; + + @GetMapping("/download/{datasetId}") + public ResponseEntity downloadFile(@PathVariable Long datasetId) throws IOException { + File downloadedFile = s3FileDownloadService.downloadFile(datasetId); + Resource fileResource = new FileSystemResource(downloadedFile); + String fileName = downloadedFile.getName(); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + URLEncoder.encode(fileName, "UTF-8") + "\"") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(fileResource); + } + +} diff --git a/src/main/java/com/capstone/favicon/dataset/domain/DataAnalysis.java b/src/main/java/com/capstone/favicon/dataset/domain/DataAnalysis.java new file mode 100644 index 0000000..d175c1c --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/domain/DataAnalysis.java @@ -0,0 +1,41 @@ +package com.capstone.favicon.dataset.domain; + +import com.capstone.favicon.user.domain.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import java.time.LocalDate; + +@Entity +@Getter +@Setter +@Table(name="data_analysis") +public class DataAnalysis { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long analysisId; + + @ManyToOne + @JoinColumn(name = "dataset_id", nullable = false) + private Dataset dataset; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + private LocalDate analysisDate; + + @Column(columnDefinition = "TEXT") + private String analysisResult; + + /* private String resultUrl; + + @Enumerated(EnumType.STRING) + private AnalysisStatus status = AnalysisStatus.REQUESTED; + + public enum AnalysisStatus { + REQUESTED, IN_PROGRESS, COMPLETED + } */ +} + diff --git a/src/main/java/com/capstone/favicon/dataset/domain/Dataset.java b/src/main/java/com/capstone/favicon/dataset/domain/Dataset.java new file mode 100644 index 0000000..6a35e41 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/domain/Dataset.java @@ -0,0 +1,72 @@ +package com.capstone.favicon.dataset.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.Set; + +@Entity +@Getter +@Setter +@Table(name="dataset") +public class Dataset { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "dataset_id") + private Long datasetId; + + private String organization; + private String title; + private String description; + private LocalDate createdDate; + private LocalDate updateDate; + private Integer view; + private Integer download; + private String license; + private String keyword; + private Boolean analysis; + + @Column(name = "name") + private String name; + + @Column(name = "s3Key", unique = true) + private String s3Key; + + @ManyToOne + @JoinColumn(name = "dataset_theme_id", nullable = false) + private DatasetTheme datasetTheme; + + public Dataset(DatasetTheme theme, String name, String title, String organization, String description) { + this.datasetTheme = theme; + this.name = name; + this.title = title; + this.organization = organization; + this.description = description; + } + + public Dataset(DatasetTheme theme, String name, String title, String organization, String description, String s3key, + LocalDate createdDate, LocalDate updateDate, Integer view, Integer download) { + this.datasetTheme = theme; + this.name = name; + this.title = title; + this.organization = organization; + this.description = description; + this.s3Key = s3key; + this.createdDate = createdDate; + this.updateDate = updateDate; + this.view = view; + this.download = download; + } + + + protected Dataset() {} + + @OneToOne(mappedBy = "dataset", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + private Resource resource; + + @OneToMany(mappedBy = "dataset", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private Set downloadSet; +} diff --git a/src/main/java/com/capstone/favicon/dataset/domain/DatasetTheme.java b/src/main/java/com/capstone/favicon/dataset/domain/DatasetTheme.java new file mode 100644 index 0000000..0209502 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/domain/DatasetTheme.java @@ -0,0 +1,26 @@ +package com.capstone.favicon.dataset.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "dataset_theme") +public class DatasetTheme { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "dataset_theme_id") + private Long datasetThemeId; + + private String theme; + private String region; + private Integer dataYear; + private String fileType; + + public Long getId() { + return datasetThemeId; + } +} diff --git a/src/main/java/com/capstone/favicon/dataset/domain/Download.java b/src/main/java/com/capstone/favicon/dataset/domain/Download.java new file mode 100644 index 0000000..2cf78b6 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/domain/Download.java @@ -0,0 +1,25 @@ +package com.capstone.favicon.dataset.domain; + +import com.capstone.favicon.user.domain.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name="download") +public class Download { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "download_id") + private Long downloadId; + + @ManyToOne + @JoinColumn(name = "dataset_id", referencedColumnName = "dataset_id") + private Dataset dataset; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; +} diff --git a/src/main/java/com/capstone/favicon/dataset/domain/FileExtension.java b/src/main/java/com/capstone/favicon/dataset/domain/FileExtension.java new file mode 100644 index 0000000..0981efd --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/domain/FileExtension.java @@ -0,0 +1,5 @@ +package com.capstone.favicon.dataset.domain; + +public enum FileExtension { + CSV, XLSX, JSON, TXT +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/dataset/domain/Region.java b/src/main/java/com/capstone/favicon/dataset/domain/Region.java new file mode 100644 index 0000000..50a750a --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/domain/Region.java @@ -0,0 +1,17 @@ +package com.capstone.favicon.dataset.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "region") +@Getter +@Setter +public class Region { + + @Id + @Column(name = "region_name", nullable = false) + private String regionName; + +} diff --git a/src/main/java/com/capstone/favicon/dataset/domain/Resource.java b/src/main/java/com/capstone/favicon/dataset/domain/Resource.java new file mode 100644 index 0000000..40f8e3a --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/domain/Resource.java @@ -0,0 +1,45 @@ +package com.capstone.favicon.dataset.domain; + +import com.capstone.favicon.dataset.domain.Dataset; +import com.capstone.favicon.dataset.domain.FileExtension; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name = "resource") +public class Resource { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "resource_id") + private Long resourceId; + + @Column(name = "resource_name", nullable = false) + private String resourceName; + + @Enumerated(EnumType.STRING) + @Column(name = "resource_type", nullable = true) + private FileExtension type; + + @Column(name = "resource_url", nullable = false) + private String resourceUrl; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnore + @JoinColumn(name = "dataset_id", referencedColumnName = "dataset_id", nullable = false) + private Dataset dataset; + + public Resource(Dataset dataset, String resourceName, FileExtension type, String resourceUrl) { + this.dataset = dataset; + this.resourceName = resourceName; + this.type = type; + this.resourceUrl = resourceUrl; + } + + public Resource() {} +} diff --git a/src/main/java/com/capstone/favicon/dataset/domain/Trend.java b/src/main/java/com/capstone/favicon/dataset/domain/Trend.java new file mode 100644 index 0000000..e813847 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/domain/Trend.java @@ -0,0 +1,33 @@ +package com.capstone.favicon.dataset.domain; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; + +@Entity +@Table(name = "trend") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class Trend { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private LocalDate rankDate; + + private Integer rank; + + private String trendStatus; // "상승", "하락", "유지" + + @ManyToOne + @JoinColumn(name = "dataset_id") + private Dataset dataset; +} + diff --git a/src/main/java/com/capstone/favicon/dataset/dto/AnalysisRequestDto.java b/src/main/java/com/capstone/favicon/dataset/dto/AnalysisRequestDto.java new file mode 100644 index 0000000..e068274 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/dto/AnalysisRequestDto.java @@ -0,0 +1,15 @@ +package com.capstone.favicon.dataset.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class AnalysisRequestDto { + private String theme1; + private String theme2; + private String region; + private String start; + private String end; + +} diff --git a/src/main/java/com/capstone/favicon/dataset/dto/AnalysisResultDto.java b/src/main/java/com/capstone/favicon/dataset/dto/AnalysisResultDto.java new file mode 100644 index 0000000..ae713ee --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/dto/AnalysisResultDto.java @@ -0,0 +1,12 @@ +package com.capstone.favicon.dataset.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Map; + +@Getter +@Setter +public class AnalysisResultDto { + private Map result; +} diff --git a/src/main/java/com/capstone/favicon/dataset/dto/DatasetThemeDto.java b/src/main/java/com/capstone/favicon/dataset/dto/DatasetThemeDto.java new file mode 100644 index 0000000..d4473a7 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/dto/DatasetThemeDto.java @@ -0,0 +1,30 @@ +package com.capstone.favicon.dataset.dto; + +import lombok.Getter; + +@Getter +public class DatasetThemeDto { + private Long datasetThemeId; + private String theme; + private String region; + private Integer dataYear; + private String fileType; + + public DatasetThemeDto(Long datasetThemeId, String theme) { + this.datasetThemeId = datasetThemeId; + this.theme = theme; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DatasetThemeDto)) return false; + DatasetThemeDto that = (DatasetThemeDto) o; + return datasetThemeId.equals(that.datasetThemeId); + } + + @Override + public int hashCode() { + return datasetThemeId.hashCode(); + } +} diff --git a/src/main/java/com/capstone/favicon/dataset/dto/SearchDto.java b/src/main/java/com/capstone/favicon/dataset/dto/SearchDto.java new file mode 100644 index 0000000..a904b60 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/dto/SearchDto.java @@ -0,0 +1,8 @@ +package com.capstone.favicon.dataset.dto; + +import lombok.Getter; + +@Getter +public class SearchDto { + private String text; +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/dataset/repository/DatasetRepository.java b/src/main/java/com/capstone/favicon/dataset/repository/DatasetRepository.java new file mode 100644 index 0000000..42b5a43 --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/repository/DatasetRepository.java @@ -0,0 +1,40 @@ +package com.capstone.favicon.dataset.repository; + +import com.capstone.favicon.dataset.domain.Dataset; +import com.capstone.favicon.dataset.domain.DatasetTheme; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface DatasetRepository extends JpaRepository { + Optional findByS3Key(String s3Key); + List findAllByOrderByDownloadDesc(); + List findTop9ByOrderByDownloadDesc(); + long countByDatasetTheme_DatasetThemeId(Long datasetThemeId); + List findByDatasetTheme_DatasetThemeId(Long datasetThemeId); + Optional findByDatasetThemeAndNameAndOrganization(DatasetTheme datasetTheme, String name, String organization); + + @Query("SELECT d FROM Dataset d JOIN FETCH d.datasetTheme") + List findAllWithTheme(); + + @Query(value = """ + SELECT * FROM dataset + WHERE title ILIKE CONCAT('%', :keyword, '%') OR description ILIKE CONCAT('%', :keyword, '%') + ORDER BY similarity(title, :keyword) DESC, created_at DESC + """, nativeQuery = true) + List searchByText(@Param("keyword") String keyword); + + @Query(value = """ + SELECT * FROM dataset + WHERE title ILIKE CONCAT('%', :keyword, '%') OR description ILIKE CONCAT('%', :keyword, '%') + AND category = :category + ORDER BY similarity(title, :keyword) DESC, created_at DESC + """, nativeQuery = true) + List searchWithCategory(@Param("keyword") String keyword, @Param("category") String category); + +} diff --git a/src/main/java/com/capstone/favicon/dataset/repository/DatasetThemeRepository.java b/src/main/java/com/capstone/favicon/dataset/repository/DatasetThemeRepository.java new file mode 100644 index 0000000..1281ebc --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/repository/DatasetThemeRepository.java @@ -0,0 +1,25 @@ +package com.capstone.favicon.dataset.repository; + +import com.capstone.favicon.dataset.domain.DatasetTheme; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface DatasetThemeRepository extends JpaRepository { + + List findByTheme(String theme); + + List findByRegionAndDataYearAndFileType(String region, int dataYear, String fileType); + + List findByRegionAndDataYear(String region, int dataYear); + List findByRegionAndFileType(String region, String fileType); + List findByDataYearAndFileType(int dataYear, String fileType); + + List findByRegion(String region); + + List findByDataYear(int dataYear); + + List findByFileType(String fileType); + + long countByTheme(String theme); +} diff --git a/src/main/java/com/capstone/favicon/dataset/repository/RegionRepository.java b/src/main/java/com/capstone/favicon/dataset/repository/RegionRepository.java new file mode 100644 index 0000000..2b621ab --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/repository/RegionRepository.java @@ -0,0 +1,14 @@ +package com.capstone.favicon.dataset.repository; + +import com.capstone.favicon.dataset.domain.Region; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface RegionRepository extends JpaRepository { + @Query("SELECT r.regionName FROM Region r") + List findAllRegionNames(); +} diff --git a/src/main/java/com/capstone/favicon/dataset/repository/ResourceRepository.java b/src/main/java/com/capstone/favicon/dataset/repository/ResourceRepository.java new file mode 100644 index 0000000..8b88c7d --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/repository/ResourceRepository.java @@ -0,0 +1,16 @@ +package com.capstone.favicon.dataset.repository; + +import com.capstone.favicon.dataset.domain.Dataset; +import com.capstone.favicon.dataset.domain.Resource; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ResourceRepository extends JpaRepository { + Optional findByDatasetAndResourceName(Dataset dataset, String resourceName); + Optional findByDatasetDatasetId(Long datasetId); + List findByDataset(Dataset dataset); +} diff --git a/src/main/java/com/capstone/favicon/dataset/repository/TrendRepository.java b/src/main/java/com/capstone/favicon/dataset/repository/TrendRepository.java new file mode 100644 index 0000000..138d7cf --- /dev/null +++ b/src/main/java/com/capstone/favicon/dataset/repository/TrendRepository.java @@ -0,0 +1,20 @@ +package com.capstone.favicon.dataset.repository; + +import com.capstone.favicon.dataset.domain.Trend; +import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +public interface TrendRepository extends JpaRepository { + @Query("SELECT t FROM Trend t WHERE t.dataset.datasetId = :datasetId AND t.rankDate = :date") + Optional findByDatasetIdAndDate(@Param("datasetId") Long datasetId, @Param("date") LocalDate date); + List findAllByRankDate(LocalDate rankDate); + @Query("SELECT t FROM Trend t WHERE t.dataset.datasetId = :datasetId AND t.rankDate BETWEEN :startDate AND :endDate") + List findByDatasetIdAndDateRange(@Param("datasetId") Long datasetId, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/user/application/DataServiceImpl.java b/src/main/java/com/capstone/favicon/user/application/DataServiceImpl.java new file mode 100644 index 0000000..e58701f --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/application/DataServiceImpl.java @@ -0,0 +1,59 @@ +package com.capstone.favicon.user.application; + +import com.capstone.favicon.dataset.domain.Dataset; +import com.capstone.favicon.dataset.repository.DatasetRepository; +import com.capstone.favicon.user.application.service.DataService; +import com.capstone.favicon.user.domain.Scrap; +import com.capstone.favicon.user.dto.ScrapResponseDto; +import com.capstone.favicon.user.repository.DataRepository; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + + +@RequiredArgsConstructor +@Service +public class DataServiceImpl implements DataService { + + @Autowired + private DataRepository dataRepository; + @Autowired + private DatasetRepository datasetRepository; + + @Override + public ScrapResponseDto addScrap(HttpServletRequest request, Long dataId) { + HttpSession session = request.getSession(); + Long userId = (Long) session.getAttribute("id"); + Dataset dataset = datasetRepository.findById(dataId).orElse(null); + if (dataset == null) { + throw new RuntimeException(); + } + Scrap scrap = new Scrap(); + scrap.setUserId(userId); + scrap.setDatasetId(dataId); + scrap.setTitle(dataset.getTitle()); + scrap.setTheme(dataset.getDatasetTheme().getTheme()); + dataRepository.save(scrap); + return new ScrapResponseDto(dataId, dataset.getTitle(), dataset.getDatasetTheme().getTheme()); + } + + @Override + public void deleteScrap(HttpServletRequest request, Long scrapId) { + HttpSession session = request.getSession(); + Long userId = (Long) session.getAttribute("id"); + Scrap scrap = dataRepository.findByScrapIdAndUserId(scrapId, userId); + dataRepository.delete(scrap); + } + + @Override + public List getScrap(HttpServletRequest request) { + HttpSession session = request.getSession(); + Long userId = (Long) session.getAttribute("id"); + return dataRepository.findAllByUserId(userId); + } + +} diff --git a/src/main/java/com/capstone/favicon/user/application/MailServiceImpl.java b/src/main/java/com/capstone/favicon/user/application/MailServiceImpl.java new file mode 100644 index 0000000..bf983d6 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/application/MailServiceImpl.java @@ -0,0 +1,41 @@ +package com.capstone.favicon.user.application; + +import com.capstone.favicon.user.application.service.MailService; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + + +@Service +public class MailServiceImpl implements MailService { + + private final JavaMailSender mailSender; + private final TemplateEngine templateEngine; + + public MailServiceImpl(JavaMailSender mailSender, TemplateEngine templateEngine) { + this.mailSender = mailSender; + this.templateEngine = templateEngine; + } + + @Override + public void send(String to, String subject, String code) { + try { + Context context = new Context(); + context.setVariable("code", code); + String htmlContent = templateEngine.process("email", context); + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, false, "UTF-8"); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(htmlContent, true); + mailSender.send(message); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/com/capstone/favicon/user/application/OTPServiceImpl.java b/src/main/java/com/capstone/favicon/user/application/OTPServiceImpl.java new file mode 100644 index 0000000..17f52a7 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/application/OTPServiceImpl.java @@ -0,0 +1,35 @@ +package com.capstone.favicon.user.application; + +import com.capstone.favicon.user.application.service.OTPService; +import com.capstone.favicon.user.application.service.RedisService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class OTPServiceImpl implements OTPService { + private final RedisService redisService; + + @Override + public String generateOTP(String email) { + String otp = ThreadLocalRandom.current().ints(0, 10) + .limit(6) + .mapToObj(String::valueOf) + .collect(Collectors.joining()); + redisService.setCode(email, otp); + return otp; + } + + @Override + public boolean verifyOTP(String email, String otp){ + String storedOtp = redisService.getCode(email); + if (storedOtp != null && storedOtp.equals(otp)) { + redisService.deleteCode(email); + return true; + } + return false; + } +} diff --git a/src/main/java/com/capstone/favicon/user/application/RedisServiceImpl.java b/src/main/java/com/capstone/favicon/user/application/RedisServiceImpl.java new file mode 100644 index 0000000..af8cb28 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/application/RedisServiceImpl.java @@ -0,0 +1,31 @@ +package com.capstone.favicon.user.application; + +import com.capstone.favicon.user.application.service.RedisService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + + +@Service +@RequiredArgsConstructor +public class RedisServiceImpl implements RedisService { + private final RedisTemplate redisTemplate; + + @Override + public void setCode(String email, String code) { + redisTemplate.opsForValue().set(email, code, 3, TimeUnit.MINUTES); + } + + @Override + public String getCode(String email) { + Object code = redisTemplate.opsForValue().get(email); + return code == null ? null : code.toString(); + } + + @Override + public void deleteCode(String email) { + redisTemplate.delete(email); + } +} diff --git a/src/main/java/com/capstone/favicon/user/application/RequestImpl.java b/src/main/java/com/capstone/favicon/user/application/RequestImpl.java new file mode 100644 index 0000000..5703da7 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/application/RequestImpl.java @@ -0,0 +1,267 @@ +package com.capstone.favicon.user.application; + +import com.capstone.favicon.config.S3Config; +import com.capstone.favicon.dataset.domain.FileExtension; +import com.capstone.favicon.user.domain.DataRequest; +import com.capstone.favicon.user.domain.Question; +import com.capstone.favicon.user.domain.Answer; +import com.capstone.favicon.user.domain.User; +import com.capstone.favicon.user.dto.DataRequestDto; +import com.capstone.favicon.user.dto.RequestStatsDto; +import com.capstone.favicon.user.repository.UserRepository; +import com.capstone.favicon.user.repository.DataRequestRepository; +import com.capstone.favicon.user.repository.QuestionRepository; +import com.capstone.favicon.user.repository.AnswerRepository; +import com.capstone.favicon.user.application.service.RequestService; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.TreeMap; + +@Service +public class RequestImpl implements RequestService { + private final DataRequestRepository dataRequestRepository; + private final QuestionRepository questionRepository; + private final AnswerRepository answerRepository; + private final UserRepository userRepository; + private final S3Config s3Config; + + public RequestImpl(DataRequestRepository dataRequestRepository,QuestionRepository questionRepository, + AnswerRepository answerRepository, UserRepository userRepository, + @Qualifier("s3Config") S3Config s3Config) { + this.dataRequestRepository = dataRequestRepository; + this.questionRepository = questionRepository; + this.answerRepository = answerRepository; + this.userRepository = userRepository; + this.s3Config = s3Config; + } + + @Override + public List getAllRequests() { + return dataRequestRepository.findAll(); + } + + @Override + @Transactional + public DataRequest createRequest(DataRequestDto dataRequestDto) { + User user = userRepository.findByUserId(dataRequestDto.getUserId()); + if (user == null) { + throw new RuntimeException("유저 아이디를 찾을 수 없음: " + dataRequestDto.getUserId()); + } + + DataRequest dataRequest = new DataRequest(); + dataRequest.setUser(user); + dataRequest.setPurpose(dataRequestDto.getPurpose()); + dataRequest.setTitle(dataRequestDto.getTitle()); + dataRequest.setContent(dataRequestDto.getContent()); + dataRequest.setUploadDate(LocalDate.now()); + try { + String uploadedUrl = s3Config.uploadFile(dataRequestDto.getFile(), "pending"); + dataRequest.setFileUrl(uploadedUrl); + } catch (IOException e) { + throw new RuntimeException("s3에 업로드 실패", e); + } + //dataRequest.setFileUrl(dataRequestDto.getFileUrl()); + dataRequest.setOrganization(dataRequestDto.getOrganization()); + dataRequest.setReviewStatus(DataRequest.ReviewStatus.PENDING); + + return dataRequestRepository.save(dataRequest); + } + + @Override + @Transactional + public DataRequest updateReviewStatus(Long requestId, DataRequest.ReviewStatus status) { + DataRequest request = dataRequestRepository.findById(requestId) + .orElseThrow(() -> new RuntimeException("요청을 찾지 못했습니다")); + + String fileUrl = request.getFileUrl(); + if (fileUrl != null) { + String key = s3Config.extractKeyFromAnyUrl(fileUrl); + System.out.println("추출된 키: " + key); + System.out.println("추출된 파일명: " + s3Config.extractFileNameFromKey(key)); + + if (status == DataRequest.ReviewStatus.APPROVED) { + // 승인시 preprocessing 폴더로 이동(테스트 완료) + String newKey = "preprocessing/" + s3Config.extractFileNameFromKey(key); + s3Config.moveFile(key, newKey); + request.setFileUrl(s3Config.generateFileUrl(newKey)); + + } else if (status == DataRequest.ReviewStatus.REJECTED) { + // 거절시 pending 폴더에 있는 파일 삭제(테스트 완료) + s3Config.deleteFileByKey(key); + request.setFileUrl(null); + } + } + request.setReviewStatus(status); + return dataRequestRepository.save(request); + } + + @Override + public List getQuestionsByUser(Long userId) { + return questionRepository.findByUser_UserId(userId); + } + + @Override + public List getAnswersByQuestion(Long questionId) { + return answerRepository.findByQuestion_User_UserId(questionId); + } + + + @Override + @Transactional + public DataRequest updateRequest(Long requestId, DataRequest updatedRequest) { + DataRequest request = dataRequestRepository.findById(requestId) + .orElseThrow(() -> new RuntimeException("요청을 찾을 수 없습니다")); + + request.setPurpose(updatedRequest.getPurpose()); + request.setTitle(updatedRequest.getTitle()); + request.setContent(updatedRequest.getContent()); + request.setFileUrl(updatedRequest.getFileUrl()); + request.setOrganization(updatedRequest.getOrganization()); + return dataRequestRepository.save(request); + } + + @Override + @Transactional + public void deleteRequest(Long requestId) { + dataRequestRepository.deleteById(requestId); + } + + @Override + @Transactional + public Question createQuestion(Question question) { + return questionRepository.save(question); + } + + @Override + @Transactional + public Question updateQuestion(Long questionId, Question updatedQuestion) { + Question question = questionRepository.findById(questionId) + .orElseThrow(() -> new RuntimeException("Question not found")); + + question.setContent(updatedQuestion.getContent()); + return questionRepository.save(question); + } + + @Override + @Transactional + public void deleteQuestion(Long questionId) { + questionRepository.deleteById(questionId); + } + + @Override + @Transactional + public Answer createAnswer(Answer answer) { + return answerRepository.save(answer); + } + + @Override + @Transactional + public Answer updateAnswer(Long answerId, Answer updatedAnswer) { + Answer answer = answerRepository.findById(answerId) + .orElseThrow(() -> new RuntimeException("답변을 찾을 수 없습니다")); + + answer.setContent(updatedAnswer.getContent()); + return answerRepository.save(answer); + } + + @Override + @Transactional + public void deleteAnswer(Long answerId) { + answerRepository.deleteById(answerId); + } + + @Override + public RequestStatsDto getRequestStats() { + LocalDate now = LocalDate.now(); + LocalDate sixMonthsAgo = now.minusMonths(5).withDayOfMonth(1); + + List allRequests = dataRequestRepository.findAll(); + + Map monthlyCumulativeCounts = new LinkedHashMap<>(); + int cumulativeSum = 0; + + Map monthlyCounts = allRequests.stream() + .filter(req -> !req.getUploadDate().isBefore(sixMonthsAgo)) + .collect(Collectors.groupingBy( + req -> req.getUploadDate().withDayOfMonth(1).toString().substring(0, 7), + TreeMap::new, + Collectors.counting() + )); + + for (int i = 5; i >= 0; i--) { + LocalDate month = now.minusMonths(i).withDayOfMonth(1); + String key = month.toString().substring(0, 7); + int monthly = monthlyCounts.getOrDefault(key, 0L).intValue(); + cumulativeSum += monthly; + monthlyCumulativeCounts.put(key, cumulativeSum); + } + + List keys = new ArrayList<>(monthlyCumulativeCounts.keySet()); + + int currentMonthTotal = monthlyCounts.getOrDefault(keys.get(keys.size() - 1), 0L).intValue(); + int previousMonthTotal = keys.size() >= 2 ? monthlyCounts.getOrDefault(keys.get(keys.size() - 2), 0L).intValue() : 0; + int growthFromLastMonth = previousMonthTotal > 0 + ? (int) Math.round(((double)(currentMonthTotal - previousMonthTotal) / previousMonthTotal) * 100) + : 0; + + int currentPending = (int) allRequests.stream() + .filter(req -> req.getReviewStatus() == DataRequest.ReviewStatus.PENDING) + .count(); + + Map monthlyPendingCounts = allRequests.stream() + .filter(req -> req.getReviewStatus() == DataRequest.ReviewStatus.PENDING) + .filter(req -> !req.getUploadDate().isBefore(sixMonthsAgo)) + .collect(Collectors.groupingBy( + req -> req.getUploadDate().withDayOfMonth(1).toString().substring(0, 7), + TreeMap::new, + Collectors.counting() + )); + + int currentMonthPending = monthlyPendingCounts.getOrDefault(keys.get(keys.size() - 1), 0L).intValue(); + int previousMonthPending = keys.size() >= 2 ? monthlyPendingCounts.getOrDefault(keys.get(keys.size() - 2), 0L).intValue() : 0; + int pendingGrowthFromLastMonth = previousMonthPending > 0 + ? (int) Math.round(((double)(currentMonthPending - previousMonthPending) / previousMonthPending) * 100) + : 0; + + return new RequestStatsDto( + currentMonthTotal, + growthFromLastMonth, + currentPending, + pendingGrowthFromLastMonth, + monthlyCumulativeCounts + ); + } + + + + + @Override + public String getFileUrlByRequestId(Long requestId) { + DataRequest dataRequest = dataRequestRepository.findById(requestId) + .orElseThrow(() -> new IllegalArgumentException("해당 ID의 요청이 존재하지 않습니다: " + requestId)); + return dataRequest.getFileUrl(); + } + + @Override + public FileExtension getFileExtensionByRequestId(Long requestId) { + DataRequest dataRequest = dataRequestRepository.findById(requestId) + .orElseThrow(() -> new IllegalArgumentException("해당 ID의 요청이 존재하지 않습니다: " + requestId)); + return extractExtension(dataRequest.getFileUrl()); + } + + private FileExtension extractExtension(String fileUrl) { + String ext = fileUrl.substring(fileUrl.lastIndexOf('.') + 1).toUpperCase(); + return FileExtension.valueOf(ext); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/user/application/UserServiceImpl.java b/src/main/java/com/capstone/favicon/user/application/UserServiceImpl.java new file mode 100644 index 0000000..316224c --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/application/UserServiceImpl.java @@ -0,0 +1,121 @@ +package com.capstone.favicon.user.application; + +import com.capstone.favicon.user.application.service.MailService; +import com.capstone.favicon.user.application.service.OTPService; +import com.capstone.favicon.user.application.service.UserService; +import com.capstone.favicon.user.domain.User; +import com.capstone.favicon.user.dto.LoginDto; +import com.capstone.favicon.user.dto.LoginResponseDto; +import com.capstone.favicon.user.dto.RegisterDto; +import com.capstone.favicon.user.repository.UserRepository; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.*; + +@RequiredArgsConstructor +@Service +public class UserServiceImpl implements UserService { + + @Autowired + private UserRepository userRepository; + @Autowired + private MailService mailService; + @Autowired + private OTPService otpService; + + @Override + public void sendCode(RegisterDto.checkEmail checkEmail) { + String email = checkEmail.getEmail(); + if (userRepository.findByEmail(email)!=null) { + throw new IllegalArgumentException("이미 존재하는 이메일입니다. 재확인해주세요."); + } + String otp = otpService.generateOTP(email); + mailService.send(email, "[Favicon] 회원가입 인증번호", otp); + } + + @Override + public void checkCode(RegisterDto.checkCode checkCode) { + String email = checkCode.getEmail(); + String code = checkCode.getCode(); + if (! otpService.verifyOTP(email, code)) { + throw new IllegalArgumentException("인증번호가 일치하지 않습니다."); + } + } + + @Override + public void join(RegisterDto registerDto) { + if (registerDto == null) { + throw new RuntimeException(); + } + Set adminEmail = Set.of("test@gmail.com"); + User user = new User(); + String email = registerDto.getEmail(); + user.setEmail(email); + user.setUsername(registerDto.getUsername()); + user.setPassword(registerDto.getPassword()); + if (adminEmail.contains(email)) { + user.setRole(1); + } + userRepository.save(user); + } + + @Override + public LoginResponseDto login(LoginDto loginDto, HttpServletRequest request) { + String email = loginDto.getEmail(); + String password = loginDto.getPassword(); + User user = userRepository.findByEmail(email); + if (user == null) { + throw new UsernameNotFoundException(email); + } + if (user.getPassword().equals(password)) { + HttpSession session = request.getSession(); + session.setAttribute("id", user.getUserId()); + return new LoginResponseDto(user.getUserId(), user.getUsername()); + } else { + throw new BadCredentialsException("Wrong password"); + } + } + + @Override + public void logout(HttpServletRequest request) { + HttpSession session = request.getSession(); + session.removeAttribute("id"); + } + + @Override + public void delete(HttpServletRequest request) { + HttpSession session = request.getSession(); + Long id = (Long) session.getAttribute("id"); + User user = userRepository.findByUserId(id); + userRepository.delete(user); + } + + @Override + public void deleteById(Long id) { + User user = userRepository.findByUserId(id); + if (user == null) { + throw new IllegalArgumentException("존재하지 않는 사용자입니다."); + } + userRepository.delete(user); + } + + + @Override + public boolean checkAdmin(HttpServletRequest request) { + HttpSession session = request.getSession(); + Long id = (Long) session.getAttribute("id"); + User user = userRepository.findByUserId(id); + Integer role = user.getRole(); + if (role == 1) { + return true; + } + return false; + } + +} diff --git a/src/main/java/com/capstone/favicon/user/application/service/DataService.java b/src/main/java/com/capstone/favicon/user/application/service/DataService.java new file mode 100644 index 0000000..56dd316 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/application/service/DataService.java @@ -0,0 +1,13 @@ +package com.capstone.favicon.user.application.service; + +import com.capstone.favicon.user.domain.Scrap; +import com.capstone.favicon.user.dto.ScrapResponseDto; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.List; + +public interface DataService { + ScrapResponseDto addScrap(HttpServletRequest request, Long dataId); + void deleteScrap(HttpServletRequest request, Long scrapId); + List getScrap(HttpServletRequest request); +} diff --git a/src/main/java/com/capstone/favicon/user/application/service/MailService.java b/src/main/java/com/capstone/favicon/user/application/service/MailService.java new file mode 100644 index 0000000..37cff6f --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/application/service/MailService.java @@ -0,0 +1,5 @@ +package com.capstone.favicon.user.application.service; + +public interface MailService { + void send(String to, String subject, String code); +} diff --git a/src/main/java/com/capstone/favicon/user/application/service/OTPService.java b/src/main/java/com/capstone/favicon/user/application/service/OTPService.java new file mode 100644 index 0000000..75aaf94 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/application/service/OTPService.java @@ -0,0 +1,6 @@ +package com.capstone.favicon.user.application.service; + +public interface OTPService { + String generateOTP(String email); + boolean verifyOTP(String email, String otp); +} diff --git a/src/main/java/com/capstone/favicon/user/application/service/RedisService.java b/src/main/java/com/capstone/favicon/user/application/service/RedisService.java new file mode 100644 index 0000000..304917e --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/application/service/RedisService.java @@ -0,0 +1,8 @@ +package com.capstone.favicon.user.application.service; + +public interface RedisService { + void setCode(String email, String code); + String getCode(String email); + + void deleteCode(String email); +} diff --git a/src/main/java/com/capstone/favicon/user/application/service/RequestService.java b/src/main/java/com/capstone/favicon/user/application/service/RequestService.java new file mode 100644 index 0000000..f1b925b --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/application/service/RequestService.java @@ -0,0 +1,34 @@ +package com.capstone.favicon.user.application.service; + +import com.capstone.favicon.dataset.domain.FileExtension; +import com.capstone.favicon.user.domain.DataRequest; +import com.capstone.favicon.user.domain.Question; +import com.capstone.favicon.user.domain.Answer; +import com.capstone.favicon.user.dto.DataRequestDto; +import com.capstone.favicon.user.dto.RequestStatsDto; + +import java.util.List; + +public interface RequestService { + RequestStatsDto getRequestStats(); + List getAllRequests(); + DataRequest createRequest(DataRequestDto dataRequestDto); + DataRequest updateReviewStatus(Long requestId, DataRequest.ReviewStatus status); + List getQuestionsByUser(Long userId); + List getAnswersByQuestion(Long questionId); + + DataRequest updateRequest(Long requestId, DataRequest updatedRequest); + void deleteRequest(Long requestId); + + Question createQuestion(Question question); + Question updateQuestion(Long questionId, Question updatedQuestion); + void deleteQuestion(Long questionId); + + Answer createAnswer(Answer answer); + Answer updateAnswer(Long answerId, Answer updatedAnswer); + void deleteAnswer(Long answerId); + + + String getFileUrlByRequestId(Long requestId); + FileExtension getFileExtensionByRequestId(Long requestId); +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/user/application/service/UserService.java b/src/main/java/com/capstone/favicon/user/application/service/UserService.java new file mode 100644 index 0000000..3a85be9 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/application/service/UserService.java @@ -0,0 +1,19 @@ +package com.capstone.favicon.user.application.service; + +import com.capstone.favicon.user.dto.LoginDto; +import com.capstone.favicon.user.dto.LoginResponseDto; +import com.capstone.favicon.user.dto.RegisterDto; +import jakarta.servlet.http.HttpServletRequest; + + + +public interface UserService { + void sendCode(RegisterDto.checkEmail checkEmail); + void checkCode(RegisterDto.checkCode checkCode); + void join(RegisterDto registerDto); + LoginResponseDto login(LoginDto loginDto, HttpServletRequest request); + void logout(HttpServletRequest request); + void delete(HttpServletRequest request); + boolean checkAdmin(HttpServletRequest request); + void deleteById(Long id); +} diff --git a/src/main/java/com/capstone/favicon/user/controller/DataAccessController.java b/src/main/java/com/capstone/favicon/user/controller/DataAccessController.java new file mode 100644 index 0000000..a1a2142 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/controller/DataAccessController.java @@ -0,0 +1,55 @@ +package com.capstone.favicon.user.controller; + + +import com.capstone.favicon.config.APIResponse; +import com.capstone.favicon.user.application.service.DataService; +import com.capstone.favicon.user.domain.Scrap; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/users") +public class DataAccessController { + + @Autowired + private DataService dataService; + + @PostMapping("/scrap/{data-id}") + public ResponseEntity> addScrap(@PathVariable("data-id") Long dataId, HttpServletRequest request) { + try { + dataService.addScrap(request, dataId); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", null)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @DeleteMapping("/scrap/{scrap-id}") + public ResponseEntity> deleteScrap(@PathVariable("scrap-id") Long scrapId, HttpServletRequest request) { + try { + dataService.deleteScrap(request, scrapId); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", null)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/scrap") + public ResponseEntity> getScraps(HttpServletRequest request) { + try { + List scraps = dataService.getScrap(request); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", scraps)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + + +} diff --git a/src/main/java/com/capstone/favicon/user/controller/RequestController.java b/src/main/java/com/capstone/favicon/user/controller/RequestController.java new file mode 100644 index 0000000..24ee3e6 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/controller/RequestController.java @@ -0,0 +1,200 @@ +package com.capstone.favicon.user.controller; + +import com.capstone.favicon.config.APIResponse; +import com.capstone.favicon.dataset.application.service.S3FileDownloadService; +import org.springframework.core.io.Resource; +import com.capstone.favicon.user.domain.DataRequest; +import com.capstone.favicon.user.dto.DataRequestDto; +import com.capstone.favicon.user.domain.Question; +import com.capstone.favicon.user.domain.Answer; +import com.capstone.favicon.user.application.service.RequestService; +import com.capstone.favicon.user.dto.RequestStatsDto; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +@RestController +@RequestMapping("/request") +@RequiredArgsConstructor +public class RequestController { + private final RequestService requestService; + + @GetMapping("/list") + public ResponseEntity> getAllRequests() { + try { + List requests = requestService.getAllRequests(); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", requests)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @PostMapping(value = "/list", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> createRequest( + @RequestPart("dataRequestDto") DataRequestDto dataRequestDto, + @RequestPart("file") MultipartFile file) { + try { + dataRequestDto.setFile(file); + DataRequest created = requestService.createRequest(dataRequestDto); + return ResponseEntity.ok().body(APIResponse.successAPI("success", created)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @PutMapping("/list/{requestId}/review") + public ResponseEntity> updateReviewStatus(@PathVariable Long requestId, @RequestParam DataRequest.ReviewStatus status) { + try { + DataRequest dataRequest = requestService.updateReviewStatus(requestId, status); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", dataRequest)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/stats") + public ResponseEntity> getRequestStats() { + try { + RequestStatsDto stats = requestService.getRequestStats(); + return ResponseEntity.ok().body(APIResponse.successAPI("success", stats)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/question") + public ResponseEntity> getQuestions(@RequestParam Long userId) { + try { + List questions = requestService.getQuestionsByUser(userId); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", questions)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @GetMapping("/answer") + public ResponseEntity> getAnswers(@RequestParam Long questionId) { + try { + List answers = requestService.getAnswersByQuestion(questionId); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", answers)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + // 요청 게시글 수정 + @PutMapping("/{requestId}") + public ResponseEntity> updateRequest(@PathVariable Long requestId, @RequestBody DataRequest updatedRequest) { + try { + DataRequest dataRequest = requestService.updateRequest(requestId, updatedRequest); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", dataRequest)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + // 요청 게시글 삭제 + @DeleteMapping("/{requestId}") + public ResponseEntity> deleteRequest(@PathVariable Long requestId) { + requestService.deleteRequest(requestId); + return ResponseEntity.noContent().build(); + } + + // 질문 작성 + @PostMapping("/question") + public ResponseEntity> createQuestion(@RequestBody Question question) { + try { + Question newQuestion = requestService.createQuestion(question); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", newQuestion)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + // 질문 수정 + @PutMapping("/question/{questionId}") + public ResponseEntity> updateQuestion(@PathVariable Long questionId, @RequestBody Question updatedQuestion) { + try { + Question newQuestion = requestService.updateQuestion(questionId, updatedQuestion); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", newQuestion)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + // 질문 삭제 + @DeleteMapping("/question/{questionId}") + public ResponseEntity> deleteQuestion(@PathVariable Long questionId) { + try { + requestService.deleteQuestion(questionId); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", null)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + // 답변 작성 + @PostMapping("/answer") + public ResponseEntity> createAnswer(@RequestBody Answer answer) { + try { + Answer newAnswer = requestService.createAnswer(answer); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", newAnswer)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + // 답변 수정 + @PutMapping("/answer/{answerId}") + public ResponseEntity> updateAnswer(@PathVariable Long answerId, @RequestBody Answer updatedAnswer) { + try { + Answer newAnswer = requestService.updateAnswer(answerId, updatedAnswer); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", newAnswer)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + // 답변 삭제 + @DeleteMapping("/answer/{answerId}") + public ResponseEntity> deleteAnswer(@PathVariable Long answerId) { + try { + requestService.deleteAnswer(answerId); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", null)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + + + @Autowired + private S3FileDownloadService s3FileDownloadService; + + @GetMapping("/download/{requestId}") + public ResponseEntity downloadDataRequestFile(@PathVariable Long requestId) throws IOException { + File downloadedFile = s3FileDownloadService.downloadFileFromDataRequest(requestId); + Resource fileResource = new FileSystemResource(downloadedFile); + String fileName = downloadedFile.getName(); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + fileName) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(fileResource); + } + + + + + + +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/user/controller/UserController.java b/src/main/java/com/capstone/favicon/user/controller/UserController.java new file mode 100644 index 0000000..8ca5034 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/controller/UserController.java @@ -0,0 +1,125 @@ +package com.capstone.favicon.user.controller; + +import com.capstone.favicon.user.application.service.UserService; +import com.capstone.favicon.config.APIResponse; +import com.capstone.favicon.user.domain.User; +import com.capstone.favicon.user.dto.LoginDto; +import com.capstone.favicon.user.dto.LoginResponseDto; +import com.capstone.favicon.user.dto.RegisterDto; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + + +@RequiredArgsConstructor +@RestController +@RequestMapping("/users") +public class UserController { + + @Autowired + private UserService userService; + + @PostMapping("/email-check") + public ResponseEntity> emailCheck(@RequestBody RegisterDto.checkEmail checkEmail) { + try { + userService.sendCode(checkEmail); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", checkEmail.getEmail())); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @PostMapping("/code-check") + public ResponseEntity> checkCode(@RequestBody RegisterDto.checkCode checkCode) { + try { + userService.checkCode(checkCode); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", checkCode.getCode())); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @PostMapping("/register") + public ResponseEntity> register(@RequestBody RegisterDto registerDto) { + try { + userService.join(registerDto); + return ResponseEntity.ok().body(APIResponse.successAPI("Success", registerDto)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + + @PostMapping("/login") + public ResponseEntity> login(@RequestBody LoginDto loginDto, HttpServletRequest request){ + try { + LoginResponseDto responseDto = userService.login(loginDto, request); + return ResponseEntity.ok().body(APIResponse.successAPI("Successfully login.", responseDto)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @PostMapping("/logout") + public ResponseEntity> logout(HttpServletRequest request){ + try { + userService.logout(request); + return ResponseEntity.ok().body(APIResponse.successAPI("Successfully logout.", null)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + + @DeleteMapping("/delete-account") + public ResponseEntity> deleteUser(HttpServletRequest request) { + try { + userService.delete(request); + return ResponseEntity.ok().body(APIResponse.successAPI("탈퇴하였습니다.", null)); + } catch (Exception e) { + String message = e.getMessage(); + return ResponseEntity.badRequest().body(APIResponse.errorAPI(message)); + } + } + + @DeleteMapping("/delete-account/{id}") + public ResponseEntity> deleteUser(@PathVariable Long id) { + try { + userService.deleteById(id); + return ResponseEntity.ok().body(APIResponse.successAPI("탈퇴하였습니다.", null)); + } catch (Exception e) { + String message = e.getMessage(); + return ResponseEntity.badRequest().body(APIResponse.errorAPI(message)); + } + } + + + @GetMapping("/session-check") + public ResponseEntity> checkSession(HttpServletRequest request) { + try { + HttpSession session = request.getSession(false); + Long id = (Long) session.getAttribute("id"); + return ResponseEntity.ok().body(APIResponse.successAPI("로그인 상태.", id)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI("로그인 상태 아님.")); + } + + } + + @GetMapping("/admin-check") + public ResponseEntity> checkAdmin(HttpServletRequest request) { + try { + boolean isAdmin = userService.checkAdmin(request); + if (isAdmin) { + return ResponseEntity.ok().body(APIResponse.successAPI("관리자", 1)); + } else { + return ResponseEntity.ok().body(APIResponse.successAPI("일반 사용자", 0)); + } + } catch (Exception e) { + return ResponseEntity.badRequest().body(APIResponse.errorAPI(e.getMessage())); + } + } + +} diff --git a/src/main/java/com/capstone/favicon/user/domain/Answer.java b/src/main/java/com/capstone/favicon/user/domain/Answer.java new file mode 100644 index 0000000..a5308b1 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/domain/Answer.java @@ -0,0 +1,32 @@ +package com.capstone.favicon.user.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import java.time.LocalDate; + +@Entity +@Getter +@Setter +@Table(name="answer") +public class Answer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long answerId; + + @ManyToOne + @JoinColumn(name = "question_id", nullable = false) + private Question question; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + // private String title; + + @Column(columnDefinition = "TEXT") + private String content; + + private LocalDate createDate; +} diff --git a/src/main/java/com/capstone/favicon/user/domain/DataRequest.java b/src/main/java/com/capstone/favicon/user/domain/DataRequest.java new file mode 100644 index 0000000..4629b9c --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/domain/DataRequest.java @@ -0,0 +1,48 @@ +package com.capstone.favicon.user.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import java.time.LocalDate; + +@Entity +@Getter +@Setter +@Table(name="data_request") +public class DataRequest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long dataRequestId; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + private String purpose; + private String title; + + @Column(columnDefinition = "TEXT") + private String content; + + private LocalDate uploadDate; + private String fileUrl; + + @Enumerated(EnumType.STRING) + private ReviewStatus reviewStatus = ReviewStatus.PENDING; + + @ManyToOne + @JoinColumn(name = "reviewed_by") + private User reviewedBy; + + private LocalDate reviewDate; + private String organization; + + public enum ReviewStatus { + PENDING, APPROVED, REJECTED + } + + public Long getUserId() { + return user.getUserId(); + } +} diff --git a/src/main/java/com/capstone/favicon/user/domain/Question.java b/src/main/java/com/capstone/favicon/user/domain/Question.java new file mode 100644 index 0000000..ae8c33c --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/domain/Question.java @@ -0,0 +1,45 @@ +package com.capstone.favicon.user.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import java.time.LocalDate; + +@Entity +@Getter +@Setter +@Table(name="question") +public class Question { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long questionId; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(columnDefinition = "TEXT") + private String content; + + private LocalDate createDate; + /* private String title; + private Integer view = 0; + + @Enumerated(EnumType.STRING) + private AnswerStatus answerStatus = AnswerStatus.PENDING; + + @Enumerated(EnumType.STRING) + private Category category; + + private String imageUrl; + + public enum AnswerStatus { + PENDING, COMPLETED + } + + public enum Category { + GENERAL, TECHNICAL, POLICY + } */ +} + diff --git a/src/main/java/com/capstone/favicon/user/domain/Scrap.java b/src/main/java/com/capstone/favicon/user/domain/Scrap.java new file mode 100644 index 0000000..63e24a8 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/domain/Scrap.java @@ -0,0 +1,21 @@ +package com.capstone.favicon.user.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name="scrap") +public class Scrap { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long scrapId; + + private Long userId; + private Long datasetId; + private String title; + private String theme; +} + diff --git a/src/main/java/com/capstone/favicon/user/domain/User.java b/src/main/java/com/capstone/favicon/user/domain/User.java new file mode 100644 index 0000000..f13d8a0 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/domain/User.java @@ -0,0 +1,38 @@ +package com.capstone.favicon.user.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + private Long userId; + + @Column(nullable = false, unique = true) + private String email; + + @Column(nullable = false) + private String username; + + @Column(nullable = false) + private String password; + + private Integer role = 0; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @PrePersist + public void prePersist() { + createdAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/user/dto/DataRequestDto.java b/src/main/java/com/capstone/favicon/user/dto/DataRequestDto.java new file mode 100644 index 0000000..b414b01 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/dto/DataRequestDto.java @@ -0,0 +1,17 @@ +package com.capstone.favicon.user.dto; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.web.multipart.MultipartFile; + +@Getter +@Setter +public class DataRequestDto { + private Long userId; + private String purpose; + private String title; + private String content; + private MultipartFile file; + private String fileUrl; + private String organization; +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/user/dto/LoginDto.java b/src/main/java/com/capstone/favicon/user/dto/LoginDto.java new file mode 100644 index 0000000..4a19179 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/dto/LoginDto.java @@ -0,0 +1,11 @@ +package com.capstone.favicon.user.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginDto { + private String email; + private String password; +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/user/dto/LoginResponseDto.java b/src/main/java/com/capstone/favicon/user/dto/LoginResponseDto.java new file mode 100644 index 0000000..84f969f --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/dto/LoginResponseDto.java @@ -0,0 +1,14 @@ +package com.capstone.favicon.user.dto; + +import lombok.Getter; + +@Getter +public class LoginResponseDto { + private Long userId; + private String username; + + public LoginResponseDto(Long userId, String username) { + this.userId = userId; + this.username = username; + } +} diff --git a/src/main/java/com/capstone/favicon/user/dto/RegisterDto.java b/src/main/java/com/capstone/favicon/user/dto/RegisterDto.java new file mode 100644 index 0000000..303d59b --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/dto/RegisterDto.java @@ -0,0 +1,25 @@ +package com.capstone.favicon.user.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RegisterDto { + + private String email; + private String username; + private String password; + + @Getter + public static class checkEmail { + private String email; + } + + @Getter + public static class checkCode { + private String email; + private String code; + } + +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/user/dto/RequestStatsDto.java b/src/main/java/com/capstone/favicon/user/dto/RequestStatsDto.java new file mode 100644 index 0000000..729f22e --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/dto/RequestStatsDto.java @@ -0,0 +1,27 @@ +package com.capstone.favicon.user.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Map; + +@Getter +@Setter +public class RequestStatsDto { + private final int currentMonthTotal; + private final int growthFromLastMonth; + private final int currentMonthPending; + private final int pendingGrowthFromLastMonth; + private final Map last6MonthsTotals; + + public RequestStatsDto(int currentMonthTotal, int growthFromLastMonth, + int currentMonthPending, int pendingGrowthFromLastMonth, + Map last6MonthsTotals) { + this.currentMonthTotal = currentMonthTotal; + this.growthFromLastMonth = growthFromLastMonth; + this.currentMonthPending = currentMonthPending; + this.pendingGrowthFromLastMonth = pendingGrowthFromLastMonth; + this.last6MonthsTotals = last6MonthsTotals; + } + +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/user/dto/ScrapResponseDto.java b/src/main/java/com/capstone/favicon/user/dto/ScrapResponseDto.java new file mode 100644 index 0000000..9c85be4 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/dto/ScrapResponseDto.java @@ -0,0 +1,16 @@ +package com.capstone.favicon.user.dto; + + +public class ScrapResponseDto { + + private Long datasetId; + private String title; + private String theme; + + public ScrapResponseDto(Long datasetId, String title, String theme) { + this.datasetId = datasetId; + this.title = title; + this.theme = theme; + } + +} diff --git a/src/main/java/com/capstone/favicon/user/repository/AnswerRepository.java b/src/main/java/com/capstone/favicon/user/repository/AnswerRepository.java new file mode 100644 index 0000000..14a58ab --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/repository/AnswerRepository.java @@ -0,0 +1,9 @@ +package com.capstone.favicon.user.repository; + +import com.capstone.favicon.user.domain.Answer; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface AnswerRepository extends JpaRepository { + List findByQuestion_User_UserId(Long questionId); +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/user/repository/DataRepository.java b/src/main/java/com/capstone/favicon/user/repository/DataRepository.java new file mode 100644 index 0000000..de0bc63 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/repository/DataRepository.java @@ -0,0 +1,14 @@ +package com.capstone.favicon.user.repository; + +import com.capstone.favicon.user.domain.Scrap; +import com.capstone.favicon.user.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface DataRepository extends JpaRepository { + List findAllByUserId(Long userId); + Scrap findByScrapIdAndUserId(Long scrapId, Long userId); +} diff --git a/src/main/java/com/capstone/favicon/user/repository/DataRequestRepository.java b/src/main/java/com/capstone/favicon/user/repository/DataRequestRepository.java new file mode 100644 index 0000000..a0bd2c7 --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/repository/DataRequestRepository.java @@ -0,0 +1,9 @@ +package com.capstone.favicon.user.repository; + +import com.capstone.favicon.user.domain.DataRequest; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface DataRequestRepository extends JpaRepository { + List findByReviewStatus(DataRequest.ReviewStatus reviewStatus); +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/user/repository/QuestionRepository.java b/src/main/java/com/capstone/favicon/user/repository/QuestionRepository.java new file mode 100644 index 0000000..dd12e6b --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/repository/QuestionRepository.java @@ -0,0 +1,9 @@ +package com.capstone.favicon.user.repository; + +import com.capstone.favicon.user.domain.Question; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface QuestionRepository extends JpaRepository { + List findByUser_UserId(Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/capstone/favicon/user/repository/UserRepository.java b/src/main/java/com/capstone/favicon/user/repository/UserRepository.java new file mode 100644 index 0000000..a2669bf --- /dev/null +++ b/src/main/java/com/capstone/favicon/user/repository/UserRepository.java @@ -0,0 +1,30 @@ +package com.capstone.favicon.user.repository; + +import com.capstone.favicon.user.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + + +@Repository +public interface UserRepository extends JpaRepository { + User findByUserId(Long userId); + User findByEmail(String email); + void deleteByUserId(Long userId); + + @Query("SELECT u.userId, u.email, u.username FROM User u WHERE u.role=0") + List getAll(); + + @Query("SELECT COUNT(*) FROM User u WHERE u.role=0") + int countAllUsers(); + + @Query("SELECT COUNT(u) FROM User u " + + "WHERE u.role=0 AND " + + "u.createdAt >= :start AND u.createdAt < :end") + int countUsersAt(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end); +} + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 83c367f..303bea6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -43,4 +43,7 @@ springdoc.default-consumes-media-type=application/json springdoc.auto-tag-classes=true springdoc.api-docs.groups.enabled=false springdoc.swagger-ui.operations-sorter=method -springdoc.swagger-ui.path=/swagger-ui.html \ No newline at end of file +springdoc.swagger-ui.path=/swagger-ui.html + +# gpt +openai.api-key=${API_KEY} \ No newline at end of file diff --git a/src/main/resources/templates/email.html b/src/main/resources/templates/email.html new file mode 100644 index 0000000..de2507e --- /dev/null +++ b/src/main/resources/templates/email.html @@ -0,0 +1,76 @@ + + + + + + FAVICON 회원가입 인증 코드 + + + +
+

FAVICON

+

회원가입 인증 코드

+

아래의 인증 코드를 입력하여 회원가입을 완료하세요.

+
[[${code}]]
+

이 코드는 3분 동안 유효합니다.

+

+
+ + diff --git a/src/test/java/com/capstone/favicon/application/UserServiceTest.java b/src/test/java/com/capstone/favicon/application/UserServiceTest.java index 76c1327..51e10a5 100644 --- a/src/test/java/com/capstone/favicon/application/UserServiceTest.java +++ b/src/test/java/com/capstone/favicon/application/UserServiceTest.java @@ -1,3 +1,3 @@ package com.capstone.favicon.application; public class UserServiceTest { -} \ No newline at end of file +} diff --git "a/\352\270\260\355\233\204_\352\260\225\354\210\230_\352\270\260\354\203\201\354\262\255.csv" "b/\352\270\260\355\233\204_\352\260\225\354\210\230_\352\270\260\354\203\201\354\262\255.csv" new file mode 100644 index 0000000..9b9dd8a Binary files /dev/null and "b/\352\270\260\355\233\204_\352\260\225\354\210\230_\352\270\260\354\203\201\354\262\255.csv" differ