diff --git a/.github/workflows/delploy-to-server.yml b/.github/workflows/delploy-to-server.yml index ce687fe..1c218d8 100644 --- a/.github/workflows/delploy-to-server.yml +++ b/.github/workflows/delploy-to-server.yml @@ -1,11 +1,10 @@ -name: CI/CD for Spring Boot +name: KernelLabs CICD on: push: - branches: - - main - - develop - + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] jobs: build: name: Build and Test @@ -48,12 +47,17 @@ jobs: name: Build and Push Docker Image runs-on: ubuntu-latest needs: build - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' steps: - name: Checkout Repository uses: actions/checkout@v4 + - name: Create application-prod.yml for Docker build + run: | + mkdir -p src/main/resources + echo "${{ secrets.PROPERTIES_PROD }}" > src/main/resources/application-prod.yml + shell: bash + - name: Log in to Docker Hub uses: docker/login-action@v2 with: @@ -74,8 +78,10 @@ jobs: name: Deploy to Server runs-on: ubuntu-latest needs: docker - steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Deploy via SSH uses: appleboy/ssh-action@v1.0.3 with: @@ -88,4 +94,7 @@ jobs: docker rm spring-app || true docker run -d --name spring-app -p 80:8080 \ -e TZ=Asia/Seoul \ - ${{ secrets.DOCKER_USERNAME }}/spring-app:latest \ No newline at end of file + -e SPRING_PROFILES_ACTIVE=prod \ + ${{ secrets.DOCKER_USERNAME }}/spring-app:latest + + diff --git a/.gitignore b/.gitignore index 2f51786..23c0cb8 100644 --- a/.gitignore +++ b/.gitignore @@ -188,4 +188,5 @@ $RECYCLE.BIN/ *.lnk # env -application-local.yml \ No newline at end of file +application-local.yml +application-prod.yml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c2b6f8f..6fe5022 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,19 @@ -# πŸ”Ή 1단계: λΉŒλ“œ ν™˜κ²½ (Gradle λΉŒλ“œ) FROM gradle:8.5-jdk17 AS builder WORKDIR /app -# Gradle 캐싱을 μœ„ν•΄ μ˜μ‘΄μ„± κ΄€λ ¨ 파일 λ¨Όμ € 볡사 COPY build.gradle settings.gradle ./ COPY gradle gradle RUN gradle build || return 0 # Gradle μΊμ‹œ 적용 -# 전체 μ†ŒμŠ€ 볡사 ν›„ λΉŒλ“œ μ‹€ν–‰ COPY . . RUN gradle clean build -x test - -# πŸ”Ή 2단계: μ‹€ν–‰ ν™˜κ²½ (JVM만 포함) FROM eclipse-temurin:17-jdk WORKDIR /app -# λΉŒλ“œλœ JAR νŒŒμΌμ„ μ‹€ν–‰ ν™˜κ²½μœΌλ‘œ 볡사 COPY --from=builder /app/build/libs/*.jar app.jar -# μ»¨ν…Œμ΄λ„ˆ μ‹€ν–‰ μ‹œ Spring Boot μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹€ν–‰ -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file +COPY --from=builder /app/src/main/resources/application-prod.yml /app/config/application-prod.yml + +ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-Dspring.config.location=classpath:/,file:/app/config/", "-jar", "app.jar"] + +EXPOSE 8080 \ No newline at end of file diff --git a/build.gradle b/build.gradle index d7c6cf1..a706060 100644 --- a/build.gradle +++ b/build.gradle @@ -26,12 +26,17 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'com.google.genai:google-genai:1.6.0' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' runtimeOnly 'com.mysql:mysql-connector-j' + } tasks.named('test') { diff --git a/src/main/java/com/kernellabs/kernellabs/application/ChatbotService.java b/src/main/java/com/kernellabs/kernellabs/application/ChatbotService.java new file mode 100644 index 0000000..7f3b6b9 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/application/ChatbotService.java @@ -0,0 +1,37 @@ +package com.kernellabs.kernellabs.application; + +import org.springframework.stereotype.Service; + +import com.kernellabs.kernellabs.infrastructure.external.GeminiApiClient; +import com.kernellabs.kernellabs.presentation.dto.request.ChatAnswerRequest; +import com.kernellabs.kernellabs.presentation.dto.response.ChatAnswerResponse; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ChatbotService { + + private final GeminiApiClient geminiApiClient; + + public ChatAnswerResponse chat(ChatAnswerRequest chatAnswerRequest) { + String prompt = String.format( + "당신은 μ˜μ„±κ΅°μ— νŠΉν™”λœ 정보 제곡 μ±—λ΄‡μž…λ‹ˆλ‹€.\n" + + "λͺ¨λ“  닡변은 μ˜μ„±κ΅°μ— λŒ€ν•œ λ‚΄μš©λ§Œμ„ 포함해야 ν•©λ‹ˆλ‹€. λ‹€λ₯Έ μ§€μ—­ μ •λ³΄λ‚˜ 일반적인 λ‚΄μš©μ€ μ–ΈκΈ‰ν•˜μ§€ λ§ˆμ„Έμš”.\n" + + "μ‚¬μš©μžμ˜ 질문이 μ˜μ„±κ΅°κ³Ό 직접적인 관련이 μ—†κ±°λ‚˜ μ˜μ„±κ΅°μ— λŒ€ν•œ μ •λ³΄λ‘œ λ‹΅λ³€ν•  수 μ—†λŠ” 경우, 'μ£„μ†‘ν•©λ‹ˆλ‹€. μ €λŠ” μ˜μ„±κ΅°μ— λŒ€ν•œ μ •λ³΄λ§Œμ„ μ œκ³΅ν•©λ‹ˆλ‹€.' 와 같이 λ‹΅λ³€ λ²”μœ„λ₯Ό λͺ…ν™•νžˆ μ•Œλ¦¬κ³  μΆ”κ°€ μ§ˆλ¬Έμ„ μœ λ„ν•˜μ„Έμš”.\n\n" + + "μ˜μ„±κ΅°μ˜ 지리, 역사, λ¬Έν™”, νŠΉμ‚°λ¬Ό, μ£Όμš” μ‚°μ—…(특히 농업), κ΄€κ΄‘μ§€, μΆ•μ œ, 인ꡬ ν˜„ν™©, κΈ°ν›„, μ •μ±…(μ›ŒμΌ€μ΄μ…˜, κ·€λ†κ·€μ΄Œ λ“±), ꡐ톡 λ“±μ˜ 정보에 쀑점을 λ‘‘λ‹ˆλ‹€.\n" + + "특히, μ›ŒμΌ€μ΄μ…˜, 슀마트 농업, μƒν™œμΈκ΅¬ μœ μž…κ³Ό κ΄€λ ¨λœ μ˜μ„±κ΅°μ˜ 상세 정보에 μš°μ„ μˆœμœ„λ₯Ό 두고 λ‹΅λ³€ν•΄ μ£Όμ„Έμš”.\n" + + "질문이 λͺ¨ν˜Έν•  경우, μ˜μ„±κ΅°κ³Ό κ΄€λ ¨λœ κ°€μž₯ κ΄€λ ¨μ„± 높은 정보λ₯Ό μš°μ„ μ μœΌλ‘œ μ œκ³΅ν•΄ μ£Όμ„Έμš”.\n\n" + + "닡변은 μ •ν™•ν•˜κ³  사싀에 κΈ°λ°˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.\n" + + "μΉœμ ˆν•˜κ³  λͺ…ν™•ν•œ μ–΄μ‘°λ‘œ λ‹΅λ³€ν•΄ μ£Όμ„Έμš”.\n" + + "ν•„μš”μ‹œ 정보λ₯Ό λͺ©λ‘μ΄λ‚˜ κ°„κ²°ν•œ 문단 ν˜•μ‹μœΌλ‘œ μ •λ¦¬ν•˜μ—¬ 가독성을 λ†’μ—¬μ£Όμ„Έμš”.\n" + + "μ‚¬μš©μžκ°€ 더 κΉŠμ€ 정보λ₯Ό 원할 경우, μΆ”κ°€ μ§ˆλ¬Έμ„ μœ λ„ν•˜λŠ” ν˜•νƒœλ‘œ λŒ€ν™”λ₯Ό μ΄λŒμ–΄ λ‚˜κ°ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.\n\n" + + "μ‚¬μš©μžμ˜ 질문: %s", + chatAnswerRequest.getQuestion()); + + String answer = geminiApiClient.askGemini(prompt); + return ChatAnswerResponse.builder().answer(answer).build(); + } + + +} diff --git a/src/main/java/com/kernellabs/kernellabs/application/PlaceService.java b/src/main/java/com/kernellabs/kernellabs/application/PlaceService.java new file mode 100644 index 0000000..d8161a6 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/application/PlaceService.java @@ -0,0 +1,112 @@ +package com.kernellabs.kernellabs.application; + +import com.kernellabs.kernellabs.domain.Place; +import com.kernellabs.kernellabs.domain.Reservation; +import com.kernellabs.kernellabs.global.exception.CustomException; +import com.kernellabs.kernellabs.global.exception.ErrorCode; +import com.kernellabs.kernellabs.infrastructure.repository.PlaceRepository; +import com.kernellabs.kernellabs.infrastructure.repository.ReservationRepository; +import com.kernellabs.kernellabs.presentation.dto.response.PlaceDetailResponse; +import com.kernellabs.kernellabs.presentation.dto.response.PlaceListResponse; +import com.kernellabs.kernellabs.presentation.dto.response.TimeSlotResponse; +import com.kernellabs.kernellabs.presentation.dto.response.enums.SlotStatus; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class PlaceService { + + private final PlaceRepository placeRepository; + private final ReservationRepository reservationRepository; + + public List getAllPlace() { + return placeRepository.findAll().stream() + .map(PlaceListResponse::from) + .collect(Collectors.toList()); + } + + public PlaceDetailResponse getPlaceDetailWithDate(Long placeId, LocalDate date, Long editingReservationId) { + // 1. μž₯μ†Œ 정보 쑰회 + Place place = placeRepository.findById(placeId) + .orElseThrow(() -> new CustomException(ErrorCode.PLACE_NOT_FOUND)); + + // 2. ν•΄λ‹Ή λ‚ μ§œ μ‹€μ œ 운영 μ‹œκ°„ 확인 + LocalTime openTime = place.getOpenTime(); + LocalTime closeTime = place.getCloseTime(); + + // 3. ν•΄λ‹Ή λ‚ μ§œμ˜ 'λͺ¨λ“ ' μ˜ˆμ•½μ„ 일단 λ‹€ κ°€μ Έμ˜¨λ‹€. + List allReservationsOnDate = reservationRepository.findByPlaceIdAndReservationDate(placeId, date); + + // 4. 'λ‚˜μ˜ μ˜ˆμ•½' μ‹œκ°„κ³Ό 'λ‹€λ₯Έ μ‚¬λžŒ μ˜ˆμ•½' μ‹œκ°„μ„ λΆ„λ¦¬ν•˜μ—¬ Set으둜 λ§Œλ“ λ‹€. + Set myReservedSlots = getSlotsForSpecificReservation(allReservationsOnDate, editingReservationId); + Set othersReservedSlots = getSlotsForOtherReservations(allReservationsOnDate, editingReservationId); + + // 5. 3κ°€μ§€ μƒνƒœλ₯Ό ν¬ν•¨ν•œ 전체 μ‹œκ°„ 슬둯 리슀트λ₯Ό μƒμ„±ν•œλ‹€. + List timeSlots = generateTimeSlotsWithStatus(openTime, closeTime, myReservedSlots, othersReservedSlots); + + // 6. μ΅œμ’… 응닡 DTOλ₯Ό λ§Œλ“€μ–΄ λ°˜ν™˜ν•œλ‹€. + return PlaceDetailResponse.of(place, timeSlots); + } + + private List generateTimeSlotsWithStatus(LocalTime openTime, LocalTime closeTime, Set mySlots, Set otherSlots) { + List slots = new ArrayList<>(); + LocalTime currentTime = openTime; + while (!currentTime.isAfter(closeTime.minusHours(1))) { + SlotStatus status; + if (mySlots.contains(currentTime)) { + status = SlotStatus.MY_RESERVATION; + } else if (otherSlots.contains(currentTime)) { + status = SlotStatus.UNAVAILABLE; + } else { + status = SlotStatus.AVAILABLE; + } + slots.add(new TimeSlotResponse(currentTime.toString(), status)); + currentTime = currentTime.plusHours(1); + } + return slots; + } + + private Set getSlotsForSpecificReservation(List reservations, Long reservationId) { + if (reservationId == null) { + return Collections.emptySet(); + } + return reservations.stream() + .filter(r -> r.getId().equals(reservationId)) + .flatMap(this::expandReservationToSlots) + .collect(Collectors.toSet()); + } + private Set getSlotsForOtherReservations(List reservations, Long reservationId) { + // 'λ‚˜μ˜ μ˜ˆμ•½ ID'κ°€ μ—†λŠ” 경우(μ‹ κ·œ μ˜ˆμ•½ λͺ¨λ“œ)μ—λŠ” λͺ¨λ“  μ˜ˆμ•½μ΄ 'λ‹€λ₯Έ μ‚¬λžŒ μ˜ˆμ•½'이 λœλ‹€. + if (reservationId == null) { + return reservations.stream() + .flatMap(this::expandReservationToSlots) + .collect(Collectors.toSet()); + } + // 'λ‚˜μ˜ μ˜ˆμ•½ ID'κ°€ μžˆλŠ” 경우(μˆ˜μ • λͺ¨λ“œ)μ—λŠ” ν•΄λ‹Ή μ˜ˆμ•½μ„ μ œμ™Έν•œλ‹€. + return reservations.stream() + .filter(r -> !r.getId().equals(reservationId)) + .flatMap(this::expandReservationToSlots) + .collect(Collectors.toSet()); + } + + private Stream expandReservationToSlots(Reservation reservation) { + List slots = new ArrayList<>(); + LocalTime current = reservation.getStartTime(); + while (current.isBefore(reservation.getEndTime())) { + slots.add(current); + current = current.plusHours(1); + } + return slots.stream(); + } +} diff --git a/src/main/java/com/kernellabs/kernellabs/application/PolicyService.java b/src/main/java/com/kernellabs/kernellabs/application/PolicyService.java new file mode 100644 index 0000000..2498692 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/application/PolicyService.java @@ -0,0 +1,41 @@ +package com.kernellabs.kernellabs.application; + +import org.springframework.stereotype.Service; + +import com.kernellabs.kernellabs.global.util.RedisUtil; +import com.kernellabs.kernellabs.infrastructure.external.GeminiApiClient; +import com.kernellabs.kernellabs.infrastructure.external.GeminiSearchClient; +import com.kernellabs.kernellabs.presentation.dto.response.PolicyResponse; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class PolicyService { + + private final String POLICY_REDIS_KEY = "policy"; + private final RedisUtil redisUtil; + private final GeminiSearchClient geminiSearchClient; + private final String prompt = """ + μ˜μ„±κ΅°μœΌλ‘œ μ΄μ£Όν•˜λ €λŠ” μ‚¬λžŒλ“€μ„ μœ„ν•œ μ΅œμ‹  ν˜œνƒ 정보λ₯Ό μ•Œλ €μ€˜. + μ£Όκ±°, ꡐ윑, κ·€λ†κ·€μ΄Œ, 볡지, μ°½μ—…, 일자리 μ§€μ›κΈˆ, μ •μ°©κΈˆ λ“± λͺ¨λ“  μ’…λ₯˜μ˜ 이주 및 μ •μ°© ν˜œνƒμ„ ν¬ν•¨ν•΄μ€˜. + 각 ν˜œνƒλ³„λ‘œ 지원 쑰건, μ‹ μ²­ 방법, λ‹΄λ‹Ή λΆ€μ„œ λ˜λŠ” κ΄€λ ¨ μ›Ήμ‚¬μ΄νŠΈ 링크 같은 상세 정보도 μ•Œλ €μ€˜. + 정보λ₯Ό λŒ€μ£Όμ œμ™€ μ†Œμ£Όμ œλ‘œ λ‚˜λˆ μ„œ μ •λ¦¬ν•΄μ€˜. + 각 μ†Œμ£Όμ œ μ•„λž˜μ— μžμ„Έν•œ μ„€λͺ…을 μΆ”κ°€ν•˜κ³ , κ΄€λ ¨ 링크가 μžˆλ‹€λ©΄ URL μ£Όμ†Œλ₯Ό λͺ…ν™•νžˆ ν¬ν•¨ν•΄μ€˜. + 각 ν•­λͺ©μ€ 쀄 λ°”κΏˆ(\n)을 μ‚¬μš©ν•΄μ„œ κ΅¬λΆ„ν•΄μ€˜. + λ‹€λ₯Έ μ§€μ—­ μ •λ³΄λ‚˜ 일반적인 λ‚΄μš©μ€ μ œμ™Έν•˜κ³ , 였직 μ˜μ„±κ΅° κ΄€λ ¨ ν˜œνƒλ§Œ λ‹€λ€„μ€˜. + """; + + public PolicyResponse getCurrentPolicy() { + String result = ""; + if(redisUtil.existData(POLICY_REDIS_KEY)){ + result = redisUtil.getData(POLICY_REDIS_KEY); + } + else{ + result = geminiSearchClient.generateAnswer(prompt); + redisUtil.setDataExpire(POLICY_REDIS_KEY, result, 10800); + } + return PolicyResponse.builder().policy(result).build(); + + } +} diff --git a/src/main/java/com/kernellabs/kernellabs/application/ReservationService.java b/src/main/java/com/kernellabs/kernellabs/application/ReservationService.java new file mode 100644 index 0000000..ab0728a --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/application/ReservationService.java @@ -0,0 +1,104 @@ +package com.kernellabs.kernellabs.application; + +import com.kernellabs.kernellabs.application.validator.ReservationValidator; +import com.kernellabs.kernellabs.domain.Place; +import com.kernellabs.kernellabs.domain.Reservation; +import com.kernellabs.kernellabs.global.exception.CustomException; +import com.kernellabs.kernellabs.global.exception.ErrorCode; +import com.kernellabs.kernellabs.infrastructure.repository.PlaceRepository; +import com.kernellabs.kernellabs.infrastructure.repository.ReservationRepository; +import com.kernellabs.kernellabs.presentation.dto.request.ReservationDeleteRequest; +import com.kernellabs.kernellabs.presentation.dto.request.ReservationRequest; +import com.kernellabs.kernellabs.presentation.dto.request.ReservationUpdateRequest; +import com.kernellabs.kernellabs.presentation.dto.request.ReservationVerityRequest; +import com.kernellabs.kernellabs.presentation.dto.response.ReservationResponse; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Transactional(readOnly = true) +@Service +@RequiredArgsConstructor +public class ReservationService { + + private final ReservationRepository reservationRepository; + private final PlaceRepository placeRepository; + private final ReservationValidator reservationValidator; + + @Transactional + public ReservationResponse createReservation(ReservationRequest request) { + // 1. μž₯μ†Œ 쑰회 + Place place = placeRepository.findById(request.getPlaceId()) + .orElseThrow(() -> new CustomException(ErrorCode.PLACE_NOT_FOUND)); + + // 2. μœ νš¨μ„± 검증 + reservationValidator.validateForCreate(place, request); + + // 3. μ—”ν‹°ν‹° 생성 + Reservation reservation = Reservation.create(place, request.getPassword(), + request.getReservationDate(), request.getTimeSlots()); + + // 4. μ˜ˆμ•½ μ €μž₯ 및 응닡 λ°˜ν™˜ + reservationRepository.save(reservation); + return ReservationResponse.from(reservation); + } + + @Transactional + public ReservationResponse getReservation(Long reservationId, ReservationVerityRequest request) { + // 1. ID둜 μ˜ˆμ•½ μ°ΎκΈ° + Reservation reservation = findReservationById(reservationId); + + // 2. λΉ„λ°€λ²ˆν˜Έμ™€ request password 비ꡐ + validatePassword(request.getPassword(), reservation.getPassword()); + + // 3.DTO λ³€ν™˜ + return ReservationResponse.from(reservation); + } + + @Transactional + public ReservationResponse updateReservation(Long reservationId, ReservationUpdateRequest request) { + // 1. μ˜ˆμ•½ 쑰회 및 λΉ„λ°€λ²ˆν˜Έ 확인 + Reservation reservation = findReservationById(reservationId); + validatePassword(request.getPassword(), reservation.getPassword()); + + // 2. λ³€κ²½ μš”μ²­ μœ νš¨μ„± 검사 + reservationValidator.validateForUpdate(reservation, request); + + // 3. μ—”ν‹°ν‹° μƒνƒœ λ³€κ²½ + reservation.updateTimes(request.getNewReservationDate(), parseStartTime(request.getNewTimeSlots()), parseEndTime(request.getNewTimeSlots())); + return ReservationResponse.from(reservation); + } + + @Transactional + public void deleteReservation(Long reservationId, ReservationDeleteRequest request) { + // 1. μ˜ˆμ•½ 쑰회 및 λΉ„λ°€λ²ˆν˜Έ 확인 + Reservation reservation = findReservationById(reservationId); + validatePassword(request.getPassword(), reservation.getPassword()); + + // 2. μ˜ˆμ•½ μ‚­μ œ + reservationRepository.delete(reservation); + } + + private LocalTime parseStartTime(List timeSlots) { + return LocalTime.parse(timeSlots.get(0), DateTimeFormatter.ofPattern("HH:mm")); + } + + private LocalTime parseEndTime(List timeSlots) { + return LocalTime.parse(timeSlots.get(timeSlots.size() - 1), DateTimeFormatter.ofPattern("HH:mm")).plusHours(1); + } + + private void validatePassword(String rawPassword, String storedPassword) { + if (!rawPassword.equals(storedPassword)) { + throw new CustomException(ErrorCode.INVALID_PASSWORD); + } + } + + private Reservation findReservationById(Long reservationId) { + return reservationRepository.findById(reservationId) + .orElseThrow(() -> new CustomException(ErrorCode.PLACE_NOT_FOUND)); + } + +} diff --git a/src/main/java/com/kernellabs/kernellabs/application/VacationService.java b/src/main/java/com/kernellabs/kernellabs/application/VacationService.java new file mode 100644 index 0000000..dd168e9 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/application/VacationService.java @@ -0,0 +1,68 @@ +package com.kernellabs.kernellabs.application; + +import org.springframework.stereotype.Service; + +import com.kernellabs.kernellabs.infrastructure.external.GeminiApiClient; +import com.kernellabs.kernellabs.presentation.dto.request.RouteRequest; +import com.kernellabs.kernellabs.presentation.dto.response.RouteResponse; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class VacationService { + + private final GeminiApiClient geminiApiClient; + + public RouteResponse getVacationRoute(RouteRequest routeRequest) { + String prompt = String.format( + """ + 당신은 μ˜μ„±κ΅° μ „λ¬Έ μ—¬ν–‰ ν”Œλž˜λ„ˆμž…λ‹ˆλ‹€. + μ‚¬μš©μžκ°€ μš”μ²­ν•œ 정보에 κΈ°λ°˜ν•˜μ—¬ λŒ€ν•œλ―Όκ΅­ 경상뢁도 μ˜μ„±κ΅°μœΌλ‘œμ˜ μƒμ„Έν•œ μ—¬ν–‰ κ³„νšμ„ μ§œμ£Όμ„Έμš”. + + --- μ‚¬μš©μž μš”μ²­ 정보 ---= + μ‹œμž‘μΌ: %s + κΈ°κ°„: %s + μ‹œκ°„λŒ€: %s + μ„ ν˜Έ λΆ„μœ„κΈ°: %s + 관심 ν™œλ™: %s + ꡐ톡: %s + λ™λ°˜μž: %s (없을 경우 λ¬΄μ‹œ) + μ˜ˆμ‚°: %s (없을 경우 λ¬΄μ‹œ) + ------------------------- + + μ—¬ν–‰ κ³„νšμ€ λ‹€μŒ μ§€μ‹œμ‚¬ν•­μ„ μ—„κ²©ν•˜κ²Œ μ§€μΌœμ„œ JSON 객체둜만 μ œκ³΅ν•΄μ•Ό ν•©λ‹ˆλ‹€. + μ„€λͺ…은 ν•œκ΅­μ–΄λ‘œ μž‘μ„±ν•˜κ³ , λ§ˆν¬λ‹€μš΄(json 블둝 포함)μ΄λ‚˜ λ‹€λ₯Έ μ„€λͺ… 없이 μˆœμˆ˜ν•œ JSON ν…μŠ€νŠΈλ§Œ 응닡해야 ν•©λ‹ˆλ‹€. + 각 μž₯μ†ŒλŠ” 'name', 'address', 'activity', 'estimated_cost', 'estimated_time' 을 λ°˜λ“œμ‹œ 포함해야 ν•©λ‹ˆλ‹€. + μ˜ˆμƒ λΉ„μš©μ€ '무료', '저렴함', '보톡', 'λΉ„μŒˆ' λ˜λŠ” ꡬ체적인 κ°€κ²©λŒ€λ₯Ό ν•œκ΅­μ–΄λ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”. + ν™œλ™ λ‚΄μš©κ³Ό μž₯μ†Œ μ„€λͺ…은 ꡬ체적이고 λ§€λ ₯적으둜 μž‘μ„±ν•΄ μ£Όμ„Έμš”. + 각 μž₯μ†Œ μ‚¬μ΄λŠ” ꡐ톡 μ†Œμš”μ‹œκ°„μ„ μ°Έκ³ ν•΄μ„œ μ°Ύμ•„μ£Όμ„Έμš”. + μ—¬ν–‰ κ³„νšμ€ μ΅œμ†Œ 1일 이상 κ΅¬μ„±λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€. + + 응닡 ν˜•μ‹ μ˜ˆμ‹œ: + { + "plan": [ + { + "day": 1, + "description": "9μ‹œ 일정", + "places": [ + { "name": "μž₯μ†Œ 이름", "address": "μ£Όμ†Œ", "activity": "ν•  것", "estimated_cost": "μ˜ˆμƒ λΉ„μš©", "estimated_time": "μ˜ˆμƒ μ‹œκ°„" } + ] + } + ] + } + """, + routeRequest.getStartDate(), + routeRequest.getDuration(), + routeRequest.getTime(), + routeRequest.getVibe(), + routeRequest.getInterests(), + routeRequest.getTransportation(), + routeRequest.getCompanion(), + routeRequest.getBudget() + ); + String answer = geminiApiClient.askGemini(prompt); + return RouteResponse.builder().route(answer).build(); + } + +} diff --git a/src/main/java/com/kernellabs/kernellabs/application/WorkService.java b/src/main/java/com/kernellabs/kernellabs/application/WorkService.java new file mode 100644 index 0000000..e1b50ce --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/application/WorkService.java @@ -0,0 +1,80 @@ +package com.kernellabs.kernellabs.application; + +import com.kernellabs.kernellabs.domain.Work; +import com.kernellabs.kernellabs.infrastructure.WorkRepository; +import com.kernellabs.kernellabs.presentation.dto.request.SurveyRequest; +import com.kernellabs.kernellabs.presentation.dto.response.SurveyResponse; +import java.util.Map; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class WorkService { + private final WorkRepository placeRepo; + + public SurveyResponse recommend(SurveyRequest req) { + // 1) μ‚¬μš©μž 벑터(u) 계산 (κΈ°μ‘΄ μ½”λ“œ μœ μ§€) + double[] u = new double[8]; + u[0] = "A".equals(req.getQ1()) ? 1 : 0; + u[1] = "B".equals(req.getQ1()) ? 1 : 0; + u[2] = "C".equals(req.getQ1()) ? 1 : 0; + u[3] = "D".equals(req.getQ1()) ? 1 : 0; + u[4] = switch(req.getQ2()) { case "A"->0.1; case "B"->0.3; case "C"->0.5; case "D"->0.75; default->1.0; }; + u[5] = switch(req.getQ3()) { case "A"->0.0; case "B"->0.33; case "C"->0.66; default->1.0; }; + u[6] = switch(req.getQ4()) { case "A"->0.0; case "B"->0.5; case "C"->1.0; default->0.0; }; + u[7] = "A".equals(req.getQ5()) ? 1 : 0; + + // 2) λͺ¨λ“  μž₯μ†Œ μŠ€μ½”μ–΄λ§ β†’ 졜고 1개 pick + Work best = placeRepo.findAll().stream() + .map(p -> Map.entry(p, calcScore(u, p))) + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElseThrow(() -> new RuntimeException("μΆ”μ²œν•  μž₯μ†Œκ°€ μ—†μŠ΅λ‹ˆλ‹€")); + + return SurveyResponse.from(best); + } + + /** + * 차이 기반 μœ μ‚¬λ„ 계산 + * - 이진(one-hot) feature: dot-product + * - 연속(norm) feature: 1 - |pref - actual| + * - pref[u6]==0 (μƒκ΄€μ—†μŒ)이면 μœ„μΉ˜ 점수 full(1점) + * - priv bonus: ν•„μš”ν•˜κ³  κ°€λŠ₯ν•˜λ©΄ +1 + */ + private double calcScore(double[] u, Work p) { + double score = 0; + + // 1) 업무 μœ ν˜•(dot) + double[] vType = { + p.getWorkDev()?1:0, + p.getWorkDoc()?1:0, + p.getWorkMeet()?1:0, + p.getWorkCall()?1:0 + }; + for (int i = 0; i < 4; i++) { + score += u[i] * vType[i]; + } + + // 2) μ‹œκ°„ 적합도 + score += 1 - Math.abs(p.getHoursNorm() - u[4]); + + // 3) 뢐빔 적합도 + score += 1 - Math.abs(p.getCrowdNorm() - u[5]); + + // 4) μœ„μΉ˜ 적합도 (μƒκ΄€μ—†μŒ β†’ full 점수) + if (u[6] == 0) { + score += 1; + } else { + score += 1 - Math.abs(p.getLocationNorm() - u[6]); + } + + // 5) 독립 곡간 ν•„μš” λ³΄λ„ˆμŠ€ + if (u[7] == 1 && p.getPrivateAvailable()) { + score += 1; + } + + return score; + } + +} diff --git a/src/main/java/com/kernellabs/kernellabs/application/validator/ReservationValidator.java b/src/main/java/com/kernellabs/kernellabs/application/validator/ReservationValidator.java new file mode 100644 index 0000000..1cad215 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/application/validator/ReservationValidator.java @@ -0,0 +1,117 @@ +package com.kernellabs.kernellabs.application.validator; + +import com.kernellabs.kernellabs.domain.Place; +import com.kernellabs.kernellabs.domain.PlaceUnavailableDay; +import com.kernellabs.kernellabs.domain.Reservation; +import com.kernellabs.kernellabs.global.exception.CustomException; +import com.kernellabs.kernellabs.global.exception.ErrorCode; +import com.kernellabs.kernellabs.infrastructure.repository.PlaceUnavailableDayRepository; +import com.kernellabs.kernellabs.infrastructure.repository.ReservationRepository; +import com.kernellabs.kernellabs.presentation.dto.request.ReservationRequest; +import com.kernellabs.kernellabs.presentation.dto.request.ReservationUpdateRequest; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ReservationValidator { + + private final ReservationRepository reservationRepository; + private final PlaceUnavailableDayRepository unavailableDayRepository; + private final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + + public void validateForCreate(Place place, ReservationRequest request) { + // 1. μ‹œκ°„ 슬둯 자체의 μœ νš¨μ„± 검증 (포맷, 연속성) + validateTimeSlots(request.getTimeSlots()); + + LocalTime requestStartTime = LocalTime.parse(request.getTimeSlots().get(0), TIME_FORMATTER); + LocalTime requestEndTime = LocalTime.parse(request.getTimeSlots().get(request.getTimeSlots().size() - 1), TIME_FORMATTER).plusHours(1); + + // 2. 운영 μ‹œκ°„ λ‚΄μ˜ μš”μ²­μΈμ§€ 검증 + validateAgainstOperatingHours(place, request.getReservationDate(), requestStartTime, requestEndTime); + + // 3. 쀑볡 μ˜ˆμ•½μ΄ μ—†λŠ”μ§€ 검증 + validateNoOverlappingReservations(place.getId(), request.getReservationDate(), requestEndTime, requestStartTime); + } + + public void validateForUpdate(Reservation reservation, ReservationUpdateRequest request) { + LocalDate newDate = request.getNewReservationDate(); + List newTimeSlots = request.getNewTimeSlots(); + validateTimeSlots(newTimeSlots); + + LocalTime newStartTime = LocalTime.parse(request.getNewTimeSlots().get(0), TIME_FORMATTER); + LocalTime newEndTime = LocalTime.parse(request.getNewTimeSlots().get(request.getNewTimeSlots().size() - 1), TIME_FORMATTER).plusHours(1); + + validateAgainstOperatingHours(reservation.getPlace(), newDate, newStartTime, newEndTime); + // 자기 μžμ‹  μ œμ™Έν•˜κ³  쀑볡 검사 + validateNoOverlappingForUpdate(reservation, newDate, newEndTime, newStartTime); } + + // μ‹œκ°„ 슬둯의 포맷과 연속성 검증 + private void validateTimeSlots(List timeSlots) { + if (timeSlots == null || timeSlots.isEmpty()) { + throw new CustomException(ErrorCode.TIME_SLOTS_EMPTY); + } + Collections.sort(timeSlots); + + for (int i = 0; i < timeSlots.size() - 1; i++) { + LocalTime current = LocalTime.parse(timeSlots.get(i), TIME_FORMATTER); + LocalTime next = LocalTime.parse(timeSlots.get(i + 1), TIME_FORMATTER); + if (!current.plusHours(1).equals(next)) { + throw new CustomException(ErrorCode.INVALID_TIME_SLOT_SEQUENCE); + } + } + } + + // ν•΄λ‹Ή λ‚ μ§œμ˜ μ‹€μ œ 운영 μ‹œκ°„ κΈ°μ€€μœΌλ‘œ μš”μ²­μ΄ μœ νš¨ν•œμ§€ 검증 + private void validateAgainstOperatingHours(Place place, LocalDate date, LocalTime requestStartTime, LocalTime requestEndTime) { + OperatingHours operatingHours = getOperatingHoursFor(place, date); + + if (requestStartTime.isBefore(operatingHours.openTime()) || requestEndTime.isAfter(operatingHours.closeTime())) { + throw new CustomException(ErrorCode.INVALID_RESERVATION_TIME); + } + } + + // 쀑볡 μ˜ˆμ•½ 검증 + private void validateNoOverlappingReservations(Long placeId, LocalDate date, LocalTime endTime, LocalTime startTime) { + if (reservationRepository.existsByPlaceIdAndReservationDateAndStartTimeBeforeAndEndTimeAfter(placeId, date, endTime, startTime)) { + throw new CustomException(ErrorCode.RESERVATION_ALREADY_EXISTS); + } + } + + // νŠΉμ • λ‚ μ§œμ˜ μ‹€μ œ 운영 μ‹œκ°„ 계산 + private OperatingHours getOperatingHoursFor(Place place, LocalDate date) { + Optional unavailableDayOpt = unavailableDayRepository.findByPlaceIdAndUnavailableDate(place.getId(), date); + + if (unavailableDayOpt.isPresent()) { + PlaceUnavailableDay unavailableDay = unavailableDayOpt.get(); + if (unavailableDay.getStartTimeOverride() == null) { + throw new CustomException(ErrorCode.RESERVATION_NOT_POSSIBLE_ON_DAY); + } + return new OperatingHours(unavailableDay.getStartTimeOverride(), unavailableDay.getEndTimeOverride()); + } else { + return new OperatingHours(place.getOpenTime(), place.getCloseTime()); + } + } + + // 운영 μ‹œκ°„μ„ λ‹΄λŠ” κ°„λ‹¨ν•œ λ ˆμ½”λ“œ + private record OperatingHours(LocalTime openTime, LocalTime closeTime) {} + + // 자기 μžμ‹ μ„ μ œμ™Έν•˜κ³  쀑볡 μ˜ˆμ•½μ„ ν™•μΈν•˜λŠ” λ©”μ„œλ“œ + private void validateNoOverlappingForUpdate(Reservation reservation, LocalDate newDate, LocalTime newEndTime, LocalTime newStartTime) { + if (reservationRepository.existsByPlaceIdAndReservationDateAndIdNotAndStartTimeBeforeAndEndTimeAfter( + reservation.getPlace().getId(), + newDate, + reservation.getId(), + newEndTime, + newStartTime + )) { + throw new CustomException(ErrorCode.RESERVATION_ALREADY_EXISTS); + } + } +} diff --git a/src/main/java/com/kernellabs/kernellabs/domain/Place.java b/src/main/java/com/kernellabs/kernellabs/domain/Place.java new file mode 100644 index 0000000..85095df --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/domain/Place.java @@ -0,0 +1,57 @@ +package com.kernellabs.kernellabs.domain; + +import com.kernellabs.kernellabs.global.common.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; +import java.time.LocalTime; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Place extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String name; + + @Column(nullable = false) + private String address; + + @Column(nullable = false) + private LocalTime openTime; + + @Column(nullable = false) + private LocalTime closeTime; + + @Column + private String thumbnailUrl; + + @Lob + private String description; + + @Column + private Integer unitPrice; + + @Builder + public Place(String name, String address, LocalTime openTime, LocalTime closeTime, String thumbnailUrl, String description, Integer unitPrice) { + this.name = name; + this.address = address; + this.openTime = openTime; + this.closeTime = closeTime; + this.thumbnailUrl = thumbnailUrl; + this.description = description; + this.unitPrice = unitPrice; + } + +} diff --git a/src/main/java/com/kernellabs/kernellabs/domain/PlaceUnavailableDay.java b/src/main/java/com/kernellabs/kernellabs/domain/PlaceUnavailableDay.java new file mode 100644 index 0000000..2c89aa5 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/domain/PlaceUnavailableDay.java @@ -0,0 +1,49 @@ +package com.kernellabs.kernellabs.domain; + +import com.kernellabs.kernellabs.global.common.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.time.LocalDate; +import java.time.LocalTime; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PlaceUnavailableDay extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "place_id", nullable = false) + private Place place; + + @Column(nullable = false) + private LocalDate unavailableDate; + + private String reason; + + // ν•΄λ‹Ή λ‚ μ§œμ— νŠΉλ³„νžˆ μ μš©ν•  운영 μ‹œκ°„ (null이면 μž₯μ†Œμ˜ κΈ°λ³Έ μ‹œκ°„μ„ 따름) + private LocalTime startTimeOverride; + private LocalTime endTimeOverride; + + @Builder + public PlaceUnavailableDay(Place place, LocalDate unavailableDate, String reason, LocalTime startTimeOverride, LocalTime endTimeOverride) { + this.place = place; + this.unavailableDate = unavailableDate; + this.reason = reason; + this.startTimeOverride = startTimeOverride; + this.endTimeOverride = endTimeOverride; + } +} diff --git a/src/main/java/com/kernellabs/kernellabs/domain/Reservation.java b/src/main/java/com/kernellabs/kernellabs/domain/Reservation.java new file mode 100644 index 0000000..3b49e4b --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/domain/Reservation.java @@ -0,0 +1,75 @@ +package com.kernellabs.kernellabs.domain; + +import com.kernellabs.kernellabs.global.common.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Reservation extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "place_id", nullable = false) + private Place place; + + @Column(nullable = false) + private String password; + + @Column(nullable = false) + private LocalDate reservationDate; + + @Column(nullable = false) + private LocalTime startTime; + + @Column(nullable = false) + private LocalTime endTime; + + @Builder + public Reservation(Place place, String password, LocalDate reservationDate, LocalTime startTime, LocalTime endTime) { + this.place = place; + this.password = password; + this.reservationDate = reservationDate; + this.startTime = startTime; + this.endTime = endTime; + } + + public static Reservation create(Place place, String password, LocalDate date, List timeSlots) { + DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm"); + LocalTime startTime = LocalTime.parse(timeSlots.get(0), timeFormatter); + LocalTime endTime = LocalTime.parse(timeSlots.get(timeSlots.size() - 1), timeFormatter).plusHours(1); + + return Reservation.builder() + .place(place) + .password(password) + .reservationDate(date) + .startTime(startTime) + .endTime(endTime) + .build(); + } + + public void updateTimes(LocalDate newDate, LocalTime newStartTime, LocalTime newEndTime) { + this.reservationDate = newDate; + this.startTime = newStartTime; + this.endTime = newEndTime; + } + +} diff --git a/src/main/java/com/kernellabs/kernellabs/domain/Work.java b/src/main/java/com/kernellabs/kernellabs/domain/Work.java new file mode 100644 index 0000000..70a5e28 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/domain/Work.java @@ -0,0 +1,44 @@ +package com.kernellabs.kernellabs.domain; + + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "work") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Work { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; // μž₯μ†Œλͺ… + private String imgUrl; // 이미지 μ£Όμ†Œ + + @Column(columnDefinition = "TEXT") + private String description; // μ„€λͺ… + private String type; // 곡유 μ˜€ν”ΌμŠ€/카페/μŠ€ν„°λ””μΉ΄νŽ˜ λ“± + private String address; // μ£Όμ†Œ + + private Boolean workDev; // κ°œλ°œΒ·μ½”λ”© 적합 μ—¬λΆ€ + private Boolean workDoc; // λ¬Έμ„œμž‘μ—… 적합 μ—¬λΆ€ + private Boolean workMeet; // νšŒμ˜Β·ν˜‘μ—… 적합 μ—¬λΆ€ + private Boolean workCall; // 톡화 적합 μ—¬λΆ€ + + private Double hoursNorm; // 지원 κ°€λŠ₯ μ‹œκ°„ μ •κ·œν™” (0.1~1.0) + private Double crowdNorm; // μ‚¬λžŒ 밀집도 (0.0~1.0) + private Double locationNorm; // μ‘°μš©β‡†κ΄€κ΄‘ (0.0~1.0) + private Boolean privateAvailable; // 독립 곡간 제곡 μ—¬λΆ€ +} diff --git a/src/main/java/com/kernellabs/kernellabs/global/config/RedisConfig.java b/src/main/java/com/kernellabs/kernellabs/global/config/RedisConfig.java new file mode 100644 index 0000000..9c9680a --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/global/config/RedisConfig.java @@ -0,0 +1,41 @@ +package com.kernellabs.kernellabs.global.config; + +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.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Value("${spring.data.redis.password}") + private String password; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(host, port); + redisStandaloneConfiguration.setHostName(host); + redisStandaloneConfiguration.setPort(port); + redisStandaloneConfiguration.setPassword(password); + return new LettuceConnectionFactory(redisStandaloneConfiguration); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory()); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + return template; + } +} \ No newline at end of file diff --git a/src/main/java/com/kernellabs/kernellabs/global/config/SwaggerConfig.java b/src/main/java/com/kernellabs/kernellabs/global/config/SwaggerConfig.java index 93eb7b6..7ea1031 100644 --- a/src/main/java/com/kernellabs/kernellabs/global/config/SwaggerConfig.java +++ b/src/main/java/com/kernellabs/kernellabs/global/config/SwaggerConfig.java @@ -6,15 +6,20 @@ import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.servers.Server; @Configuration public class SwaggerConfig { @Bean public OpenAPI openAPI(){ + Server server = new Server(); + server.setDescription("Production Server"); + return new OpenAPI() .components(new Components()) - .info(apiInfo()); + .info(apiInfo()) + .addServersItem(server); } private Info apiInfo(){ @@ -23,4 +28,4 @@ private Info apiInfo(){ .description("Improfessor API Documentation") .version("1.0"); } -} +} \ No newline at end of file diff --git a/src/main/java/com/kernellabs/kernellabs/global/config/WebClientConfig.java b/src/main/java/com/kernellabs/kernellabs/global/config/WebClientConfig.java new file mode 100644 index 0000000..ba60e07 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/global/config/WebClientConfig.java @@ -0,0 +1,16 @@ +package com.kernellabs.kernellabs.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient geminiWebClient() { + return WebClient.builder() + .baseUrl("https://generativelanguage.googleapis.com") + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/kernellabs/kernellabs/global/config/WebConfig.java b/src/main/java/com/kernellabs/kernellabs/global/config/WebConfig.java new file mode 100644 index 0000000..e602647 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/global/config/WebConfig.java @@ -0,0 +1,17 @@ +package com.kernellabs.kernellabs.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:5173", "https://kernel-labs-fee.vercel.app/", "https://uscode.porogramr.site/") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") + .allowedHeaders("*") + .allowCredentials(true); + } +} \ No newline at end of file diff --git a/src/main/java/com/kernellabs/kernellabs/global/exception/ErrorCode.java b/src/main/java/com/kernellabs/kernellabs/global/exception/ErrorCode.java index e2b1793..faff81a 100644 --- a/src/main/java/com/kernellabs/kernellabs/global/exception/ErrorCode.java +++ b/src/main/java/com/kernellabs/kernellabs/global/exception/ErrorCode.java @@ -28,7 +28,14 @@ public enum ErrorCode { INTERNAL_SERVER_ERROR("9999", "μ„œλ²„ λ‚΄λΆ€ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", HttpStatus.INTERNAL_SERVER_ERROR), EXTERNAL_SERVICE_ERROR("9901", "μ™ΈλΆ€ μ„œλΉ„μŠ€ 연동 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", HttpStatus.INTERNAL_SERVER_ERROR), - ; + // custom error + PLACE_NOT_FOUND("2001", "ν•΄λ‹Ή μž₯μ†Œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.", HttpStatus.NOT_FOUND), + TIME_SLOTS_EMPTY("2002", "μ˜ˆμ•½ μ‹œκ°„μ„ μ„ νƒν•΄μ£Όμ„Έμš”.", HttpStatus.BAD_REQUEST ), + INVALID_TIME_SLOT_SEQUENCE("2003", "μ˜ˆμ•½ μ‹œκ°„μ€ μ—°μ†λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.", HttpStatus.BAD_REQUEST), + RESERVATION_NOT_POSSIBLE_ON_DAY("2004", "μ„ νƒν•˜μ‹  λ‚ μ§œλŠ” μ˜ˆμ•½μ΄ λΆˆκ°€λŠ₯ν•©λ‹ˆλ‹€.", HttpStatus.CONFLICT), + INVALID_RESERVATION_TIME("2005", "μ˜ˆμ•½ μš”μ²­ μ‹œκ°„μ΄ 운영 μ‹œκ°„ λ²”μœ„λ₯Ό λ²—μ–΄λ‚©λ‹ˆλ‹€.", HttpStatus.BAD_REQUEST), + RESERVATION_ALREADY_EXISTS("2006", "ν•΄λ‹Ή μ‹œκ°„μ— 이미 μ˜ˆμ•½μ΄ μ‘΄μž¬ν•©λ‹ˆλ‹€.", HttpStatus.CONFLICT), + INVALID_PASSWORD("2007", "λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", HttpStatus.FORBIDDEN); private final HttpStatus status; private final String code; diff --git a/src/main/java/com/kernellabs/kernellabs/global/exception/GlobalExceptionHandler.java b/src/main/java/com/kernellabs/kernellabs/global/exception/GlobalExceptionHandler.java index 408ee2f..8011b2d 100644 --- a/src/main/java/com/kernellabs/kernellabs/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/kernellabs/kernellabs/global/exception/GlobalExceptionHandler.java @@ -13,7 +13,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -@RestControllerAdvice + @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) diff --git a/src/main/java/com/kernellabs/kernellabs/global/util/RedisUtil.java b/src/main/java/com/kernellabs/kernellabs/global/util/RedisUtil.java new file mode 100644 index 0000000..6a84fcd --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/global/util/RedisUtil.java @@ -0,0 +1,35 @@ +package com.kernellabs.kernellabs.global.util; + +import java.time.Duration; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class RedisUtil { + + private final StringRedisTemplate redisTemplate; + + public String getData(String key) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + return valueOperations.get(key); + } + + public boolean existData(String key) { + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + } + + public void setDataExpire(String key, String value, long duration) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + Duration expireDuration = Duration.ofSeconds(duration); + valueOperations.set(key, value, expireDuration); + } + + public void deleteData(String key) { + redisTemplate.delete(key); + } +} \ No newline at end of file diff --git a/src/main/java/com/kernellabs/kernellabs/infrastructure/WorkRepository.java b/src/main/java/com/kernellabs/kernellabs/infrastructure/WorkRepository.java new file mode 100644 index 0000000..ba48d8e --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/infrastructure/WorkRepository.java @@ -0,0 +1,8 @@ +package com.kernellabs.kernellabs.infrastructure; + +import com.kernellabs.kernellabs.domain.Work; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface WorkRepository extends JpaRepository { + +} diff --git a/src/main/java/com/kernellabs/kernellabs/infrastructure/external/GeminiApiClient.java b/src/main/java/com/kernellabs/kernellabs/infrastructure/external/GeminiApiClient.java new file mode 100644 index 0000000..1359aef --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/infrastructure/external/GeminiApiClient.java @@ -0,0 +1,60 @@ +package com.kernellabs.kernellabs.infrastructure.external; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GeminiApiClient { + + private final WebClient geminiWebClient; + + @Value("${gemini.api.key}") + private String apiKey; + + public String askGemini(String question) { + + GeminiDto.Request request = GeminiDto.Request.builder() + .contents(List.of( + GeminiDto.Request.Content.builder() + .parts(List.of( + GeminiDto.Request.Content.Part.builder() + .text(question) + .build() + )) + .build() + )) + .build(); + + GeminiDto.Response geminiResponse = geminiWebClient.post() + .uri("/v1beta/models/gemini-2.0-flash:generateContent?key=" + apiKey) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .bodyValue(request) + .retrieve() + .bodyToMono(GeminiDto.Response.class) + .onErrorResume(e -> { + log.error("Gemini 호좜 μ‹€νŒ¨", e); + return Mono.empty(); + }) + .block(); + + try { + return geminiResponse.getCandidates().get(0).getContent().getParts().get(0).getText(); + } catch (Exception e) { + log.error("Gemini λ‹΅λ³€ parsing μ‹€νŒ¨", e); + return "닡변을 생성할 수 μ—†μŠ΅λ‹ˆλ‹€"; + } + + } + +} diff --git a/src/main/java/com/kernellabs/kernellabs/infrastructure/external/GeminiDto.java b/src/main/java/com/kernellabs/kernellabs/infrastructure/external/GeminiDto.java new file mode 100644 index 0000000..3f0a1b3 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/infrastructure/external/GeminiDto.java @@ -0,0 +1,69 @@ +package com.kernellabs.kernellabs.infrastructure.external; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class GeminiDto { + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Request { + @JsonProperty("contents") + private List contents; + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Content { + + @JsonProperty("parts") + private List parts; + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Part { + @JsonProperty("text") + private String text; + } + } + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class Response { + private List candidates; + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class Candidate { + private Content content; + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class Content { + private List parts; + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class Part { + private String text; + } + } + } + } +} diff --git a/src/main/java/com/kernellabs/kernellabs/infrastructure/external/GeminiSearchClient.java b/src/main/java/com/kernellabs/kernellabs/infrastructure/external/GeminiSearchClient.java new file mode 100644 index 0000000..b53be79 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/infrastructure/external/GeminiSearchClient.java @@ -0,0 +1,46 @@ +package com.kernellabs.kernellabs.infrastructure.external; + +import autovalue.shaded.com.google.common.collect.ImmutableList; +import com.google.genai.Client; +import com.google.genai.types.GenerateContentConfig; +import com.google.genai.types.GenerateContentResponse; +import com.google.genai.types.GoogleSearch; +import com.google.genai.types.Tool; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class GeminiSearchClient { + + private final Client client; + private final Tool googleSearchTool; + private final String modelName; + + public GeminiSearchClient( + @Value("${gemini.api.key}") String apiKey, + @Value("${gemini.model:gemini-2.5-flash}") String modelName + ) { + this.client = Client.builder() + .apiKey(apiKey) + .build(); + + this.googleSearchTool = Tool.builder() + .googleSearch(GoogleSearch.builder().build()) + .build(); + + this.modelName = modelName; + } + + public String generateAnswer(String prompt) { + GenerateContentConfig config = GenerateContentConfig.builder() + .tools(ImmutableList.of(googleSearchTool)) + .build(); + + // ← μ—¬κΈ°λ₯Ό client.models()κ°€ μ•„λ‹ˆλΌ client.models 둜 μ ‘κ·Ό + GenerateContentResponse res = client.models + .generateContent(modelName, prompt, config); + + return res.text(); + } + +} diff --git a/src/main/java/com/kernellabs/kernellabs/infrastructure/repository/PlaceRepository.java b/src/main/java/com/kernellabs/kernellabs/infrastructure/repository/PlaceRepository.java new file mode 100644 index 0000000..11d0c2e --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/infrastructure/repository/PlaceRepository.java @@ -0,0 +1,10 @@ +package com.kernellabs.kernellabs.infrastructure.repository; + +import com.kernellabs.kernellabs.domain.Place; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PlaceRepository extends JpaRepository { + +} diff --git a/src/main/java/com/kernellabs/kernellabs/infrastructure/repository/PlaceUnavailableDayRepository.java b/src/main/java/com/kernellabs/kernellabs/infrastructure/repository/PlaceUnavailableDayRepository.java new file mode 100644 index 0000000..bd80b65 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/infrastructure/repository/PlaceUnavailableDayRepository.java @@ -0,0 +1,12 @@ +package com.kernellabs.kernellabs.infrastructure.repository; + +import com.kernellabs.kernellabs.domain.PlaceUnavailableDay; +import java.time.LocalDate; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PlaceUnavailableDayRepository extends JpaRepository { + Optional findByPlaceIdAndUnavailableDate(Long placeId, LocalDate date); +} diff --git a/src/main/java/com/kernellabs/kernellabs/infrastructure/repository/ReservationRepository.java b/src/main/java/com/kernellabs/kernellabs/infrastructure/repository/ReservationRepository.java new file mode 100644 index 0000000..f74284d --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/infrastructure/repository/ReservationRepository.java @@ -0,0 +1,35 @@ +package com.kernellabs.kernellabs.infrastructure.repository; + +import com.kernellabs.kernellabs.domain.Reservation; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ReservationRepository extends JpaRepository { + boolean existsByPlaceIdAndReservationDateAndStartTimeBeforeAndEndTimeAfter( + Long placeId, + LocalDate date, + LocalTime endTime, + LocalTime startTime + ); + + // μ˜ˆμ•½ λ³€κ²½ μ‹œ, κ²ΉμΉ˜λŠ” μ˜ˆμ•½μ΄ μžˆλŠ”μ§€ 확인 (자기 μžμ‹  μ œμ™Έ) + boolean existsByPlaceIdAndReservationDateAndIdNotAndStartTimeBeforeAndEndTimeAfter( + Long placeId, + LocalDate date, + Long reservationId, + LocalTime endTime, + LocalTime startTime + ); + + List findByPlaceIdAndReservationDate(Long placeId, LocalDate date); + + // νŠΉμ • μž₯μ†Œμ™€ λ‚ μ§œμ˜ λͺ¨λ“  μ˜ˆμ•½ 쑰회 (νŠΉμ • μ˜ˆμ•½μ„ μ œμ™Έ) + List findByPlaceIdAndReservationDateAndIdNot(Long placeId, LocalDate date, Long reservationId); +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/controller/ChatbotController.java b/src/main/java/com/kernellabs/kernellabs/presentation/controller/ChatbotController.java new file mode 100644 index 0000000..4f015a2 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/controller/ChatbotController.java @@ -0,0 +1,33 @@ +package com.kernellabs.kernellabs.presentation.controller; + +import java.util.List; + +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 com.kernellabs.kernellabs.application.ChatbotService; +import com.kernellabs.kernellabs.global.common.ApiResponse; +import com.kernellabs.kernellabs.presentation.dto.request.ChatAnswerRequest; +import com.kernellabs.kernellabs.presentation.dto.response.ChatAnswerResponse; +import com.kernellabs.kernellabs.presentation.dto.response.PlaceListResponse; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/chats") +public class ChatbotController { + + private final ChatbotService chatbotService; + + @PostMapping("") + public ResponseEntity> chat( + @RequestBody ChatAnswerRequest chatAnswerRequest + ) { + ChatAnswerResponse chatAnswerResponse = chatbotService.chat(chatAnswerRequest); + return ResponseEntity.ok(ApiResponse.success(chatAnswerResponse )); + } +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/controller/GeminiController.java b/src/main/java/com/kernellabs/kernellabs/presentation/controller/GeminiController.java new file mode 100644 index 0000000..67575ae --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/controller/GeminiController.java @@ -0,0 +1,37 @@ +package com.kernellabs.kernellabs.presentation.controller; + +import com.kernellabs.kernellabs.infrastructure.external.GeminiSearchClient; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +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; + +@RestController +@RequestMapping("/api/genie") +@AllArgsConstructor +public class GeminiController { + private final GeminiSearchClient geminiSearchClient; + + @PostMapping("/chat") + public ResponseEntity chat(@Valid @RequestBody ChatRequest req) { + String answer = geminiSearchClient.generateAnswer(req.getPrompt()); + return ResponseEntity.ok(new ChatResponse(answer)); + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ChatRequest { + private String prompt; + } + + @Data @NoArgsConstructor @AllArgsConstructor + public static class ChatResponse { + private String answer; + } +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/controller/PlaceController.java b/src/main/java/com/kernellabs/kernellabs/presentation/controller/PlaceController.java new file mode 100644 index 0000000..49ddc7d --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/controller/PlaceController.java @@ -0,0 +1,42 @@ +package com.kernellabs.kernellabs.presentation.controller; + +import com.kernellabs.kernellabs.application.PlaceService; +import com.kernellabs.kernellabs.global.common.ApiResponse; +import com.kernellabs.kernellabs.presentation.dto.response.PlaceDetailResponse; +import com.kernellabs.kernellabs.presentation.dto.response.PlaceListResponse; +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/places") +@RequiredArgsConstructor +public class PlaceController { + + private final PlaceService placeService; + + @GetMapping("") + public ResponseEntity getAllPlaces() { + List response = placeService.getAllPlace(); + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @GetMapping("/{placeId}") + public ResponseEntity> getPlace(@PathVariable Long placeId, + @RequestParam(required = false) @DateTimeFormat(iso = ISO.DATE) LocalDate date, + @RequestParam(required = false) Long editingReservationId) { + // λ‚ μ§œ νŒŒλΌλ―Έν„°κ°€ μ—†μœΌλ©΄ 였늘 λ‚ μ§œ κΈ°λ³Έκ°’ + LocalDate targetDate = (date == null) ? LocalDate.now() : date; + + PlaceDetailResponse response = placeService.getPlaceDetailWithDate(placeId, targetDate, editingReservationId); + return ResponseEntity.ok(ApiResponse.success(response)); + } +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/controller/PolicyController.java b/src/main/java/com/kernellabs/kernellabs/presentation/controller/PolicyController.java new file mode 100644 index 0000000..55505d3 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/controller/PolicyController.java @@ -0,0 +1,27 @@ +package com.kernellabs.kernellabs.presentation.controller; + +import java.util.List; + +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 com.kernellabs.kernellabs.application.PolicyService; +import com.kernellabs.kernellabs.global.common.ApiResponse; +import com.kernellabs.kernellabs.presentation.dto.response.PolicyResponse; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/policies") +@RequiredArgsConstructor +public class PolicyController { + private final PolicyService policyService; + + @GetMapping("") + public ResponseEntity> getCurrentPolicy() { + PolicyResponse policyResponse = policyService.getCurrentPolicy(); + return ResponseEntity.ok(ApiResponse.success(policyResponse)); + } +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/controller/ReservationController.java b/src/main/java/com/kernellabs/kernellabs/presentation/controller/ReservationController.java new file mode 100644 index 0000000..41e5740 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/controller/ReservationController.java @@ -0,0 +1,55 @@ +package com.kernellabs.kernellabs.presentation.controller; + +import com.kernellabs.kernellabs.application.ReservationService; +import com.kernellabs.kernellabs.global.common.ApiResponse; +import com.kernellabs.kernellabs.presentation.dto.request.ReservationDeleteRequest; +import com.kernellabs.kernellabs.presentation.dto.request.ReservationRequest; +import com.kernellabs.kernellabs.presentation.dto.request.ReservationUpdateRequest; +import com.kernellabs.kernellabs.presentation.dto.request.ReservationVerityRequest; +import com.kernellabs.kernellabs.presentation.dto.response.ReservationResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/reservations") +@RequiredArgsConstructor +public class ReservationController { + + private final ReservationService reservationService; + + @PostMapping("") + public ResponseEntity> createReservation(@RequestBody @Valid ReservationRequest request) { + ReservationResponse response = reservationService.createReservation(request); + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @PostMapping("/{reservationId}") + public ResponseEntity> getReservation(@PathVariable Long reservationId, + @Valid @RequestBody ReservationVerityRequest request) { + ReservationResponse response = reservationService.getReservation(reservationId, request); + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @PatchMapping("/{reservationId}") + public ResponseEntity> updateReservation(@PathVariable Long reservationId, + @Valid @RequestBody ReservationUpdateRequest request) { + ReservationResponse response = reservationService.updateReservation(reservationId, request); + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @DeleteMapping("/{reservationId}") + public ResponseEntity deleteReservation(@PathVariable Long reservationId, + @Valid @RequestBody ReservationDeleteRequest request) { + reservationService.deleteReservation(reservationId, request); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/controller/VacationController.java b/src/main/java/com/kernellabs/kernellabs/presentation/controller/VacationController.java new file mode 100644 index 0000000..d15ef13 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/controller/VacationController.java @@ -0,0 +1,48 @@ +package com.kernellabs.kernellabs.presentation.controller; + +import com.kernellabs.kernellabs.application.WorkService; +import com.kernellabs.kernellabs.presentation.dto.request.SurveyRequest; +import com.kernellabs.kernellabs.presentation.dto.response.SurveyResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.kernellabs.kernellabs.application.VacationService; +import com.kernellabs.kernellabs.global.common.ApiResponse; +import com.kernellabs.kernellabs.presentation.dto.request.RouteRequest; +import com.kernellabs.kernellabs.presentation.dto.response.RouteResponse; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +public class VacationController { + + private final VacationService vacationService; + + @PostMapping("/vacations") + public ResponseEntity> getVacationRoute( + @RequestBody @Valid RouteRequest routeRequest + ) { + RouteResponse routeResponse = vacationService.getVacationRoute(routeRequest); + return ResponseEntity.ok(ApiResponse.success(routeResponse)); + } + + @RestController + @RequestMapping("/api/work") + @AllArgsConstructor + public static class WorkController { + private final WorkService recService; + + @PostMapping("/recommend") + public ResponseEntity> recommend(@RequestBody SurveyRequest req) { + SurveyResponse surveyResponse= recService.recommend(req); + return ResponseEntity.ok(ApiResponse.success(surveyResponse)); + } + } +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/controller/WorkController.java b/src/main/java/com/kernellabs/kernellabs/presentation/controller/WorkController.java new file mode 100644 index 0000000..e9822c3 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/controller/WorkController.java @@ -0,0 +1,28 @@ +package com.kernellabs.kernellabs.presentation.controller; + +import com.kernellabs.kernellabs.application.WorkService; +import com.kernellabs.kernellabs.global.common.ApiResponse; +import com.kernellabs.kernellabs.presentation.dto.request.SurveyRequest; +import com.kernellabs.kernellabs.presentation.dto.response.SurveyResponse; +import lombok.AllArgsConstructor; +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; + +@RestController +@RequestMapping("/api") +@AllArgsConstructor +public class WorkController { + + private final WorkService workService; + + @PostMapping("/recommend") + public ResponseEntity> recommend(@RequestBody SurveyRequest req) { + SurveyResponse surveyResponse = workService.recommend(req); + return ResponseEntity.ok(ApiResponse.success(surveyResponse)); + + } + +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ChatAnswerRequest.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ChatAnswerRequest.java new file mode 100644 index 0000000..85121cc --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ChatAnswerRequest.java @@ -0,0 +1,12 @@ +package com.kernellabs.kernellabs.presentation.dto.request; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ChatAnswerRequest { + @NotNull + private String question; +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ReservationDeleteRequest.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ReservationDeleteRequest.java new file mode 100644 index 0000000..6601a9c --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ReservationDeleteRequest.java @@ -0,0 +1,15 @@ +package com.kernellabs.kernellabs.presentation.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ReservationDeleteRequest { + + @NotBlank(message = "λΉ„λ°€λ²ˆν˜ΈλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€.") + @Pattern(regexp = "\\d{4}", message = "λΉ„λ°€λ²ˆν˜ΈλŠ” 4자리 μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€.") + private String password; +} \ No newline at end of file diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ReservationRequest.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ReservationRequest.java new file mode 100644 index 0000000..f10a6f1 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ReservationRequest.java @@ -0,0 +1,32 @@ +package com.kernellabs.kernellabs.presentation.dto.request; + +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import java.time.LocalDate; +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ReservationRequest { + + @NotNull(message = "μž₯μ†Œ IDλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€.") + private Long placeId; + + @NotBlank(message = "λΉ„λ°€λ²ˆν˜ΈλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€.") + @Pattern(regexp = "\\d{4}", message = "λΉ„λ°€λ²ˆν˜ΈλŠ” 4자리 μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€.") + private String password; + + @NotNull(message = "μ˜ˆμ•½ λ‚ μ§œλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€.") + @FutureOrPresent(message = "κ³Όκ±° λ‚ μ§œλŠ” μ˜ˆμ•½ν•  수 μ—†μŠ΅λ‹ˆλ‹€.") + private LocalDate reservationDate; + + @NotEmpty(message = "μ˜ˆμ•½ μ‹œκ°„μ€ μ΅œμ†Œ 1개 이상 선택해야 ν•©λ‹ˆλ‹€.") + @Size(max = 3, message = "μ˜ˆμ•½μ€ μ΅œλŒ€ 3μ‹œκ°„κΉŒμ§€ κ°€λŠ₯ν•©λ‹ˆλ‹€.") + private List timeSlots; +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ReservationUpdateRequest.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ReservationUpdateRequest.java new file mode 100644 index 0000000..c7a4bc2 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ReservationUpdateRequest.java @@ -0,0 +1,30 @@ +package com.kernellabs.kernellabs.presentation.dto.request; + +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import java.time.LocalDate; +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ReservationUpdateRequest { + + @NotBlank(message = "λΉ„λ°€λ²ˆν˜ΈλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€.") + @Pattern(regexp = "\\d{4}", message = "λΉ„λ°€λ²ˆν˜ΈλŠ” 4자리 μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€.") + private String password; + + @NotNull(message = "λ³€κ²½ν•  λ‚ μ§œλ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.") + @FutureOrPresent(message = "κ³Όκ±° λ‚ μ§œλ‘œλŠ” λ³€κ²½ν•  수 μ—†μŠ΅λ‹ˆλ‹€.") + private LocalDate newReservationDate; + + @NotEmpty(message = "λ³€κ²½ν•  μ˜ˆμ•½ μ‹œκ°„μ€ μ΅œμ†Œ 1개 이상 선택해야 ν•©λ‹ˆλ‹€.") + @Size(max = 3, message = "μ˜ˆμ•½μ€ μ΅œλŒ€ 3μ‹œκ°„κΉŒμ§€ κ°€λŠ₯ν•©λ‹ˆλ‹€.") + private List newTimeSlots; + +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ReservationVerityRequest.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ReservationVerityRequest.java new file mode 100644 index 0000000..122ea2f --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/ReservationVerityRequest.java @@ -0,0 +1,16 @@ +package com.kernellabs.kernellabs.presentation.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ReservationVerityRequest { + + @NotBlank(message = "λΉ„λ°€λ²ˆν˜ΈλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€.") + @Pattern(regexp = "\\d{4}", message = "λΉ„λ°€λ²ˆν˜ΈλŠ” 4자리 μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€.") + private String password; + +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/RouteRequest.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/RouteRequest.java new file mode 100644 index 0000000..6d14520 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/RouteRequest.java @@ -0,0 +1,26 @@ +package com.kernellabs.kernellabs.presentation.dto.request; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class RouteRequest { + @NotNull + private String startDate; + @NotNull + private String duration; + @NotNull + private String time; + @NotNull + private String vibe; + @NotNull + private String interests; + @NotNull + private String transportation; + private String companion; + private String budget; +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/SurveyRequest.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/SurveyRequest.java new file mode 100644 index 0000000..2484030 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/request/SurveyRequest.java @@ -0,0 +1,14 @@ +package com.kernellabs.kernellabs.presentation.dto.request; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class SurveyRequest { + private String q1; // A,B,C,D + private String q2; // A~E + private String q3; // A~D + private String q4; // A~D + private String q5; // A(예), B(μ•„λ‹ˆμ˜€) +} \ No newline at end of file diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/ChatAnswerResponse.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/ChatAnswerResponse.java new file mode 100644 index 0000000..fb717fa --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/ChatAnswerResponse.java @@ -0,0 +1,10 @@ +package com.kernellabs.kernellabs.presentation.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ChatAnswerResponse { + private String answer; +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/PlaceDetailResponse.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/PlaceDetailResponse.java new file mode 100644 index 0000000..2000b00 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/PlaceDetailResponse.java @@ -0,0 +1,35 @@ +package com.kernellabs.kernellabs.presentation.dto.response; + +import com.kernellabs.kernellabs.domain.Place; +import java.time.LocalTime; +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class PlaceDetailResponse { + private Long id; + private String name; + private String address; + private String thumbnailUrl; + private String description; + private LocalTime openTime; + private LocalTime closeTime; + private Integer unitPrice; + private List timeSlots; // μ‹œκ°„ν‘œ 정보 + + public static PlaceDetailResponse of(Place place, List timeSlots) { + return PlaceDetailResponse.builder() + .id(place.getId()) + .name(place.getName()) + .address(place.getAddress()) + .thumbnailUrl(place.getThumbnailUrl()) + .description(place.getDescription()) + .openTime(place.getOpenTime()) + .closeTime(place.getCloseTime()) + .unitPrice(place.getUnitPrice()) + .timeSlots(timeSlots) + .build(); + } +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/PlaceListResponse.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/PlaceListResponse.java new file mode 100644 index 0000000..d6a9026 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/PlaceListResponse.java @@ -0,0 +1,25 @@ +package com.kernellabs.kernellabs.presentation.dto.response; + +import com.kernellabs.kernellabs.domain.Place; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class PlaceListResponse { + + private final Long id; + private final String thumbnailUrl; + private final String name; + private final String address; + + public static PlaceListResponse from(Place place) { + return PlaceListResponse.builder() + .id(place.getId()) + .thumbnailUrl(place.getThumbnailUrl()) + .name(place.getName()) + .address(place.getAddress()) + .build(); + } + +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/PolicyResponse.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/PolicyResponse.java new file mode 100644 index 0000000..197ad56 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/PolicyResponse.java @@ -0,0 +1,10 @@ +package com.kernellabs.kernellabs.presentation.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class PolicyResponse { + private String policy; +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/ReservationResponse.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/ReservationResponse.java new file mode 100644 index 0000000..04aa8cc --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/ReservationResponse.java @@ -0,0 +1,33 @@ +package com.kernellabs.kernellabs.presentation.dto.response; + +import com.kernellabs.kernellabs.domain.Reservation; +import java.time.LocalDate; +import java.time.LocalTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ReservationResponse { + + private final Long reservationId; + private final Long placeId; + private final String placeName; + private final LocalDate reservationDate; + private final LocalTime startTime; + private final LocalTime endTime; + + public static ReservationResponse from(Reservation reservation) { + return ReservationResponse.builder() + .reservationId(reservation.getId()) + .placeId(reservation.getPlace().getId()) + .placeName(reservation.getPlace().getName()) + .reservationDate(reservation.getReservationDate()) + .startTime(reservation.getStartTime()) + .endTime(reservation.getEndTime()) + .build(); + } +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/RouteResponse.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/RouteResponse.java new file mode 100644 index 0000000..b0f451c --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/RouteResponse.java @@ -0,0 +1,10 @@ +package com.kernellabs.kernellabs.presentation.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class RouteResponse { + private String route; +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/SurveyResponse.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/SurveyResponse.java new file mode 100644 index 0000000..04fbfd2 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/SurveyResponse.java @@ -0,0 +1,25 @@ +package com.kernellabs.kernellabs.presentation.dto.response; + +import com.kernellabs.kernellabs.domain.Work; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class SurveyResponse { + private String name; // μž₯μ†Œλͺ… + private String description; // μ„€λͺ… + private String type; // 곡유 μ˜€ν”ΌμŠ€/카페/μŠ€ν„°λ””μΉ΄νŽ˜ λ“± + private String address; // μ£Όμ†Œ + private String imgUrl; + + public static SurveyResponse from(Work work) { + return new SurveyResponse( + work.getName(), + work.getDescription(), + work.getType(), + work.getAddress(), + work.getImgUrl() + ); + } +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/TimeSlotResponse.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/TimeSlotResponse.java new file mode 100644 index 0000000..c59c6f5 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/TimeSlotResponse.java @@ -0,0 +1,14 @@ +package com.kernellabs.kernellabs.presentation.dto.response; + +import com.kernellabs.kernellabs.presentation.dto.response.enums.SlotStatus; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TimeSlotResponse { + + private String time; + private SlotStatus status; + +} diff --git a/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/enums/SlotStatus.java b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/enums/SlotStatus.java new file mode 100644 index 0000000..6ed4116 --- /dev/null +++ b/src/main/java/com/kernellabs/kernellabs/presentation/dto/response/enums/SlotStatus.java @@ -0,0 +1,7 @@ +package com.kernellabs.kernellabs.presentation.dto.response.enums; + +public enum SlotStatus { + AVAILABLE, // μ˜ˆμ•½ κ°€λŠ₯ (아무도 μ˜ˆμ•½ μ•ˆ 함) + UNAVAILABLE, // μ˜ˆμ•½ λΆˆκ°€ (λ‹€λ₯Έ μ‚¬λžŒμ΄ μ˜ˆμ•½ν•¨) + MY_RESERVATION // λ‚˜μ˜ μ˜ˆμ•½ (선택/ν•΄μ œ κ°€λŠ₯) +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2e71195..417b50e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,7 +2,7 @@ spring: profiles: active: local datasource: - url: jdbc:mysql://localhost:3306/kernellabs?allowPublicKeyRetrieval=true&useSSL=false + url: ${MYSQL_URL} username: ${MYSQL_USERNAME} password: ${MYSQL_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver @@ -15,13 +15,31 @@ spring: properties: hibernate: format_sql: true + + data: + redis: + host: ${REDIS_HOST} + password: ${REDIS_PASSWORD} + port: 6379 logging: level: - org.hibernate.sql: debug + root: INFO # 전체 둜그 μ΅œμ†Œ INFO 이상 기둝 :contentReference[oaicite:0]{index=0} + org.springframework.web: INFO # Spring MVC λ””μŠ€νŒ¨μ²˜ μ„œλΈ”λ¦Ώ 둜그 :contentReference[oaicite:1]{index=1} + +gemini: + api: + key: ${GEMINI_API_KEY} --- spring: config: activate: on-profile: local - import: application-local.yml \ No newline at end of file + import: application-local.yml + +--- +spring: + config: + activate: + on-profile: prod + import: application-prod.yml \ No newline at end of file