Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
8eab3b4
[feature] #4 레이어 구조 설정
sohyeonjung Jun 27, 2025
b6caaa7
[Feature] #4 레이어 구조 설정
sohyeonjung Jun 27, 2025
609aeef
[feature] #8 geminiDto 추가
sohyeonjung Jun 27, 2025
646e846
[cd] #9 - cd 수정
sohyeonjung Jun 27, 2025
0b55566
[CD] #9 - cd 수정
sohyeonjung Jun 27, 2025
a2a8983
[CD] #9 - Dockerfile 수정
sohyeonjung Jun 27, 2025
7ddc1d7
[cd] #9 - deploy 조건 변경
sohyeonjung Jun 27, 2025
e748759
[fix] #9 - deploy 줄바꿈 변경
sohyeonjung Jun 27, 2025
3d19a3d
[CD] #9 - Dockerfile 수정
sohyeonjung Jun 27, 2025
1f1f616
Merge branch 'develop' into feature/8-aiService
sohyeonjung Jun 27, 2025
766e019
[feat] #8 - Ai route 추천 controller 추가
sohyeonjung Jun 27, 2025
a8c27fb
[feat] #8 - webclient config 추가
sohyeonjung Jun 27, 2025
5d495d0
[feat] #8 - AI 루트 추천 서비스 기능 추가
sohyeonjung Jun 27, 2025
501da2c
[feat] #8 - 관광루트추천AI prompt 변경
sohyeonjung Jun 27, 2025
bddc998
[FEATURE] #8 관광 루트 추천 AI 서비스
sohyeonjung Jun 27, 2025
d011d1f
[fix] #15 - swagger version upgrade
sohyeonjung Jun 27, 2025
7cd89b5
[FIX] #15 - swagger version upgrade
sohyeonjung Jun 27, 2025
233b14f
[feat] #13 워케이션 장소 추천 API
PoroGramr Jun 27, 2025
09dfa46
[feat] #13 워케이션 장소 추천 API 로직 수정
PoroGramr Jun 27, 2025
d478d4b
[feature] #13 워케이션 장소 추천 API
PoroGramr Jun 27, 2025
e71c6b9
[feat] #18 - CORS 설정
sohyeonjung Jun 27, 2025
9cec952
[FEATURE] #18 CORS 설정
sohyeonjung Jun 27, 2025
6f6b958
[feat] #10 - reservation 관련 entity 생성
seoyeon-jung Jun 27, 2025
48cd7ff
[feat] #10 - respotiroy 생성
seoyeon-jung Jun 27, 2025
083049f
[feat] #10 - request, response 생성
seoyeon-jung Jun 27, 2025
7c18a7a
[feat] #10 - 예약하기 API 구현
seoyeon-jung Jun 27, 2025
bd8582a
[feat] CORS 해결을 위한 front 주소 추가
sohyeonjung Jun 27, 2025
89024a7
[feat] CORS 해결을 위한 front 주소 추가
sohyeonjung Jun 27, 2025
88d5c0d
[feat] #10 - 예약 변경 request 생성
seoyeon-jung Jun 27, 2025
d00f11f
[feat] #10 - 예약 수정 메소드 추가
seoyeon-jung Jun 27, 2025
8318e52
[feat] #10 - 예약 변경 API 구현
seoyeon-jung Jun 27, 2025
d6c9003
[feat] #10 - 예약 삭제 API 구현
seoyeon-jung Jun 27, 2025
275506b
[feat] #10 - 예약 조회 API 구현
seoyeon-jung Jun 27, 2025
451fe19
Merge pull request #21 from Kernel360Labs/feature/10-rentalservice
seoyeon-jung Jun 27, 2025
ea57c94
[feat] #22 - 장소 리스트 조회 시 response 생성
seoyeon-jung Jun 27, 2025
e017d82
[feat] #22 - 장소 개별 조회 시 response 생성
seoyeon-jung Jun 27, 2025
5a65582
[feat] #22 - 장소 조회 API 구현
seoyeon-jung Jun 27, 2025
bd21df2
[FEATURE] #22 장소 조회 API
seoyeon-jung Jun 27, 2025
0dc6340
[feat] #13 워케이션 장소 추천 엔티티 수정
PoroGramr Jun 27, 2025
c8d08d4
[feat] #13 워케이션 장소 추천 엔티티 수정
PoroGramr Jun 27, 2025
c236fc8
[feat] #25 chatbot 기능 추가
sohyeonjung Jun 28, 2025
66c02f4
[Feature] #25 chatbot 기능 추가
sohyeonjung Jun 28, 2025
25f68fb
[feat] #27 swagger https 연결 추가
sohyeonjung Jun 28, 2025
bd6c4e0
[feat] #27 domain cors 설정 추가
sohyeonjung Jun 28, 2025
832316d
[Feature] #27 swagger https 연결 추가
sohyeonjung Jun 28, 2025
9b1b548
[feat] #30 - 스케줄 추가한 response 생성
seoyeon-jung Jun 28, 2025
9e2c917
[chore] #30 - 필요없는 response 삭제
seoyeon-jung Jun 28, 2025
75422b8
[feat] #30 - 예약가능 여부 포함 조회 API 구현
seoyeon-jung Jun 28, 2025
1edbe2d
Merge pull request #31 from Kernel360Labs/feature/30-placeService
seoyeon-jung Jun 28, 2025
3bf712f
[feat] #32 - WebConfig에 PATCH 추가
seoyeon-jung Jun 28, 2025
9dff47c
[fix] #32 - response 수정 (status 추가)
seoyeon-jung Jun 28, 2025
b8ae8d1
[fix] #32 - 전체 시간대 조회 API 수정 (예약 수정 시 접근 가능하도록)
seoyeon-jung Jun 28, 2025
28e67e4
[fix] #32 - 예약 조회 시 유효성 검사 추가
seoyeon-jung Jun 28, 2025
701358f
[BUG] 예약 CRUD 버그 수정
seoyeon-jung Jun 28, 2025
3c128c4
[feat] #29 제미나이 그라운딩
PoroGramr Jun 28, 2025
bd2f134
[feat] #29 제미나이 그라운딩
PoroGramr Jun 28, 2025
721e222
[feat] #29 redis 설정 추가
sohyeonjung Jun 28, 2025
d856e8c
[feat] #29 gemini search policy 기능 추가
sohyeonjung Jun 28, 2025
f569f33
[feat]#29 실시간 검색 레디스 캐싱
PoroGramr Jun 28, 2025
692b061
[feat]#29 실시간 검색 레디스 캐싱
PoroGramr Jun 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions .github/workflows/delploy-to-server.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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
-e SPRING_PROFILES_ACTIVE=prod \
${{ secrets.DOCKER_USERNAME }}/spring-app:latest


3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,5 @@ $RECYCLE.BIN/
*.lnk

# env
application-local.yml
application-local.yml
application-prod.yml
13 changes: 5 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
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
7 changes: 6 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}


}
112 changes: 112 additions & 0 deletions src/main/java/com/kernellabs/kernellabs/application/PlaceService.java
Original file line number Diff line number Diff line change
@@ -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<PlaceListResponse> 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<Reservation> allReservationsOnDate = reservationRepository.findByPlaceIdAndReservationDate(placeId, date);

// 4. '나의 예약' 시간과 '다른 사람 예약' 시간을 분리하여 Set으로 만든다.
Set<LocalTime> myReservedSlots = getSlotsForSpecificReservation(allReservationsOnDate, editingReservationId);
Set<LocalTime> othersReservedSlots = getSlotsForOtherReservations(allReservationsOnDate, editingReservationId);

// 5. 3가지 상태를 포함한 전체 시간 슬롯 리스트를 생성한다.
List<TimeSlotResponse> timeSlots = generateTimeSlotsWithStatus(openTime, closeTime, myReservedSlots, othersReservedSlots);

// 6. 최종 응답 DTO를 만들어 반환한다.
return PlaceDetailResponse.of(place, timeSlots);
}

private List<TimeSlotResponse> generateTimeSlotsWithStatus(LocalTime openTime, LocalTime closeTime, Set<LocalTime> mySlots, Set<LocalTime> otherSlots) {
List<TimeSlotResponse> 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<LocalTime> getSlotsForSpecificReservation(List<Reservation> 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<LocalTime> getSlotsForOtherReservations(List<Reservation> 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<LocalTime> expandReservationToSlots(Reservation reservation) {
List<LocalTime> slots = new ArrayList<>();
LocalTime current = reservation.getStartTime();
while (current.isBefore(reservation.getEndTime())) {
slots.add(current);
current = current.plusHours(1);
}
return slots.stream();
}
}
Original file line number Diff line number Diff line change
@@ -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();

}
}
Loading