diff --git a/.DS_Store b/.DS_Store index dd46a99..5770197 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index c2065bc..e48b6be 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ out/ /.nb-gradle/ ### VS Code ### -.vscode/ +.vscode/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index b68e796..56f4f0e 100644 --- a/build.gradle +++ b/build.gradle @@ -24,15 +24,38 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-validation' - developmentOnly 'org.springframework.boot:spring-boot-devtools' - compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa:3.0.4' + implementation 'org.springframework.boot:spring-boot-starter-web:3.1.0' + testImplementation 'org.projectlombok:lombok:1.18.26' + compileOnly 'org.projectlombok:lombok:1.18.26' + runtimeOnly 'com.mysql:mysql-connector-j:8.0.32' + annotationProcessor 'org.projectlombok:lombok:1.18.26' + testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.0' + + implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' + implementation 'jakarta.validation:jakarta.validation-api:3.0.2' + + implementation 'com.squareup.okhttp3:okhttp:4.9.3' + + // Spring Doc + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4' + + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security:3.0.4' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // chatGPT API + implementation 'io.github.flashvayne:chatgpt-spring-boot-starter:1.0.4' + + // JSON 라이브러리 + implementation 'org.json:json:20230227' + + implementation 'org.springframework:spring-test:6.0.6' + testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.0' } tasks.named('test') { diff --git a/src/main/java/com/bamboo/log/diary/api/DiaryController.java b/src/main/java/com/bamboo/log/diary/api/DiaryController.java new file mode 100644 index 0000000..0b4e1c5 --- /dev/null +++ b/src/main/java/com/bamboo/log/diary/api/DiaryController.java @@ -0,0 +1,33 @@ +package com.bamboo.log.diary.api; + +import com.bamboo.log.diary.dto.request.CreateDiaryRequest; +import com.bamboo.log.diary.service.diary.DiaryService; +import com.bamboo.log.utils.ResponseHandler; +import com.bamboo.log.utils.dto.ResponseForm; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/diary") +@Tag(name = "Diary API", description = "일기 관련 API") +public class DiaryController { + + private final DiaryService diaryService; + + @Operation(summary = "랜덤 주제 조회") + @PostMapping("/create") + public ResponseEntity lookupRandomTopics(@RequestBody CreateDiaryRequest createDiaryRequest) { + try { + return diaryService.createDiary(createDiaryRequest); + } catch (Exception e) { + return ResponseHandler.create500Error(new ResponseForm(), e); + } + } +} diff --git a/src/main/java/com/bamboo/log/diary/domain/Diary.java b/src/main/java/com/bamboo/log/diary/domain/Diary.java new file mode 100644 index 0000000..c820a2c --- /dev/null +++ b/src/main/java/com/bamboo/log/diary/domain/Diary.java @@ -0,0 +1,33 @@ +package com.bamboo.log.diary.domain; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Table(name = "diaries") +public class Diary { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "diary_id") + private Long id; + + @Column(nullable = false) + private String userId; + + @Column(columnDefinition = "TEXT", nullable = false) + private String context; + + @Column(nullable = false) + private LocalDateTime createdAt; + +} diff --git a/src/main/java/com/bamboo/log/diary/domain/TodaySummary.java b/src/main/java/com/bamboo/log/diary/domain/TodaySummary.java new file mode 100644 index 0000000..24af228 --- /dev/null +++ b/src/main/java/com/bamboo/log/diary/domain/TodaySummary.java @@ -0,0 +1,29 @@ +package com.bamboo.log.diary.domain; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Builder; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "today_summaries") +public class TodaySummary { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Column(nullable = false) + private Long diaryId; + + @Lob + @Column(columnDefinition = "LONGBLOB", nullable = false) + private byte[] imageData; + +} + diff --git a/src/main/java/com/bamboo/log/diary/dto/request/CreateDiaryRequest.java b/src/main/java/com/bamboo/log/diary/dto/request/CreateDiaryRequest.java new file mode 100644 index 0000000..084708d --- /dev/null +++ b/src/main/java/com/bamboo/log/diary/dto/request/CreateDiaryRequest.java @@ -0,0 +1,9 @@ +package com.bamboo.log.diary.dto.request; + +import jakarta.validation.constraints.NotEmpty; + +import java.util.List; + +public record CreateDiaryRequest(@NotEmpty(message = "User's Id shouldn't be empty") String userId, + @NotEmpty(message = "Diary Description shouldn't be empty") String diaryDetail) { +} diff --git a/src/main/java/com/bamboo/log/diary/dto/response/CreateDiaryResponse.java b/src/main/java/com/bamboo/log/diary/dto/response/CreateDiaryResponse.java new file mode 100644 index 0000000..9e994c0 --- /dev/null +++ b/src/main/java/com/bamboo/log/diary/dto/response/CreateDiaryResponse.java @@ -0,0 +1,8 @@ +package com.bamboo.log.diary.dto.response; + +import jakarta.validation.constraints.NotEmpty; + +import java.time.LocalDateTime; + +public record CreateDiaryResponse(@NotEmpty(message = "created shouldn't be empty") LocalDateTime created) { +} diff --git a/src/main/java/com/bamboo/log/diary/repository/DiaryRepository.java b/src/main/java/com/bamboo/log/diary/repository/DiaryRepository.java new file mode 100644 index 0000000..416db42 --- /dev/null +++ b/src/main/java/com/bamboo/log/diary/repository/DiaryRepository.java @@ -0,0 +1,7 @@ +package com.bamboo.log.diary.repository; + +import com.bamboo.log.diary.domain.Diary; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DiaryRepository extends JpaRepository { +} diff --git a/src/main/java/com/bamboo/log/diary/repository/TodaySummaryRepository.java b/src/main/java/com/bamboo/log/diary/repository/TodaySummaryRepository.java new file mode 100644 index 0000000..4093d83 --- /dev/null +++ b/src/main/java/com/bamboo/log/diary/repository/TodaySummaryRepository.java @@ -0,0 +1,7 @@ +package com.bamboo.log.diary.repository; + +import com.bamboo.log.diary.domain.TodaySummary; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TodaySummaryRepository extends JpaRepository { +} diff --git a/src/main/java/com/bamboo/log/diary/service/diary/DiaryService.java b/src/main/java/com/bamboo/log/diary/service/diary/DiaryService.java new file mode 100644 index 0000000..50cb4ac --- /dev/null +++ b/src/main/java/com/bamboo/log/diary/service/diary/DiaryService.java @@ -0,0 +1,10 @@ +package com.bamboo.log.diary.service.diary; + +import com.bamboo.log.diary.dto.request.CreateDiaryRequest; +import org.springframework.http.ResponseEntity; + +public interface DiaryService { + + ResponseEntity createDiary(CreateDiaryRequest createDiaryRequest); + +} diff --git a/src/main/java/com/bamboo/log/diary/service/diary/DiaryServiceImpl.java b/src/main/java/com/bamboo/log/diary/service/diary/DiaryServiceImpl.java new file mode 100644 index 0000000..5c201fd --- /dev/null +++ b/src/main/java/com/bamboo/log/diary/service/diary/DiaryServiceImpl.java @@ -0,0 +1,70 @@ +package com.bamboo.log.diary.service.diary; + +import com.bamboo.log.diary.domain.Diary; +import com.bamboo.log.diary.dto.request.CreateDiaryRequest; +import com.bamboo.log.diary.dto.response.CreateDiaryResponse; +import com.bamboo.log.diary.repository.DiaryRepository; +import com.bamboo.log.diary.repository.TodaySummaryRepository; +import com.bamboo.log.diary.service.mock.UserService; +import com.bamboo.log.diary.service.summary.TodaySummaryService; +import com.bamboo.log.utils.ResponseHandler; +import com.bamboo.log.utils.dto.ResponseForm; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.time.LocalDateTime; + +@Service +@Transactional +@RequiredArgsConstructor +public class DiaryServiceImpl implements DiaryService { + + private final UserService userService; + private final DiaryRepository diaryRepository; + private final TodaySummaryService todaySummaryService; + + @Override + public ResponseEntity createDiary(CreateDiaryRequest createDiaryRequest) { + + // User 객체 검증 로직 + // 해당 User 객체 찾을 수 없을 경우, 에러 반환 + if(userService.existsById(createDiaryRequest.userId())) { + return ResponseHandler.create404Error(new ResponseForm(), new EntityNotFoundException("잘못된 유저 정보입니다.")); + } + + // 해당 시점의 localDateTime 저장 + LocalDateTime localDateTime = LocalDateTime.now(); + + // alice api 연결 함수 호출 + + // 데이터베이스에 저장 + Diary diary = saveDiary(createDiaryRequest, localDateTime); + + byte[] todayImage = todaySummaryService.createTodaySummaryImage(createDiaryRequest.diaryDetail()); + + // todaySummaryImage 데이터베이스에 저장 + try { + todaySummaryService.saveTodaySummaryImage(todayImage, diary.getId()); + } catch (IOException e) { + return ResponseHandler.create500Error(new ResponseForm(), e); + } + + // 201 code 반환 + return ResponseHandler.create201Response(new ResponseForm(), new CreateDiaryResponse(localDateTime)); + } + + private Diary saveDiary(CreateDiaryRequest createDiaryRequest, LocalDateTime localDateTime) { + Diary diary = Diary.builder() + .userId(createDiaryRequest.userId()) + .context(createDiaryRequest.diaryDetail()) + .createdAt(localDateTime) + .build(); + + return diaryRepository.save(diary); + } + +} diff --git a/src/main/java/com/bamboo/log/diary/service/mock/MockUserService.java b/src/main/java/com/bamboo/log/diary/service/mock/MockUserService.java new file mode 100644 index 0000000..86824f2 --- /dev/null +++ b/src/main/java/com/bamboo/log/diary/service/mock/MockUserService.java @@ -0,0 +1,12 @@ +package com.bamboo.log.diary.service.mock; + +import org.springframework.stereotype.Service; + +@Service +public class MockUserService implements UserService { + + @Override + public boolean existsById(String userId) { + return true; + } +} diff --git a/src/main/java/com/bamboo/log/diary/service/mock/UserService.java b/src/main/java/com/bamboo/log/diary/service/mock/UserService.java new file mode 100644 index 0000000..0b37f9a --- /dev/null +++ b/src/main/java/com/bamboo/log/diary/service/mock/UserService.java @@ -0,0 +1,5 @@ +package com.bamboo.log.diary.service.mock; + +public interface UserService { + boolean existsById(String userId); +} diff --git a/src/main/java/com/bamboo/log/diary/service/summary/TodaySummaryService.java b/src/main/java/com/bamboo/log/diary/service/summary/TodaySummaryService.java new file mode 100644 index 0000000..598d786 --- /dev/null +++ b/src/main/java/com/bamboo/log/diary/service/summary/TodaySummaryService.java @@ -0,0 +1,11 @@ +package com.bamboo.log.diary.service.summary; + +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +public interface TodaySummaryService { + + void saveTodaySummaryImage(byte[] image, Long diaryId) throws IOException; + byte[] createTodaySummaryImage(String prompt); +} diff --git a/src/main/java/com/bamboo/log/diary/service/summary/TodaySummaryServiceImpl.java b/src/main/java/com/bamboo/log/diary/service/summary/TodaySummaryServiceImpl.java new file mode 100644 index 0000000..06df2dd --- /dev/null +++ b/src/main/java/com/bamboo/log/diary/service/summary/TodaySummaryServiceImpl.java @@ -0,0 +1,46 @@ +package com.bamboo.log.diary.service.summary; + +import com.bamboo.log.diary.domain.TodaySummary; +import com.bamboo.log.diary.repository.TodaySummaryRepository; +import com.bamboo.log.elice.service.ImageGenerationService; +import com.bamboo.log.utils.ResponseHandler; +import com.bamboo.log.utils.dto.ResponseForm; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.Optional; + +@Service +@Transactional +@RequiredArgsConstructor +public class TodaySummaryServiceImpl implements TodaySummaryService { + + private final TodaySummaryRepository todaySummaryRepository; + private final ImageGenerationService imageGenerationService; + + @Override + public void saveTodaySummaryImage(byte[] image, Long diaryId) throws IOException { + TodaySummary todaySummary = TodaySummary.builder() + .diaryId(diaryId) + .imageData(image) + .build(); + + todaySummaryRepository.save(todaySummary); + } + + @Override + public byte[] createTodaySummaryImage(String prompt) { + try { + byte[] imageBytes = imageGenerationService.generateImage(prompt); + + return imageBytes; + } catch (IOException e) { + throw new RuntimeException("이미지 생성 중에 문제가 발생했습니다."); + } + + } + +} diff --git a/src/main/java/com/bamboo/log/elice/config/RestTemplateConfig.java b/src/main/java/com/bamboo/log/elice/config/RestTemplateConfig.java new file mode 100644 index 0000000..0ea3f07 --- /dev/null +++ b/src/main/java/com/bamboo/log/elice/config/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package com.bamboo.log.elice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/com/bamboo/log/elice/service/ImageGenerationService.java b/src/main/java/com/bamboo/log/elice/service/ImageGenerationService.java new file mode 100644 index 0000000..d7aa11b --- /dev/null +++ b/src/main/java/com/bamboo/log/elice/service/ImageGenerationService.java @@ -0,0 +1,58 @@ +package com.bamboo.log.elice.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import okhttp3.*; +import okhttp3.MediaType; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class ImageGenerationService { + + private final RestTemplate restTemplate; + private final String apiKey; + private final String apiUrl; + + public ImageGenerationService(RestTemplate restTemplate, + @Value("${elice.api.token}") String apiKey, + @Value("${elice.api.url}") String apiUrl) { + this.restTemplate = restTemplate; + this.apiKey = apiKey; + this.apiUrl = apiUrl; + } + + public byte[] generateImage(String prompt) throws IOException { + OkHttpClient client = new OkHttpClient(); + + String requestBody = String.format("{\"prompt\":\"%s\",\"style\":\"polaroid\"}", prompt); + + MediaType mediaType = MediaType.parse("application/json"); + RequestBody body = RequestBody.create(requestBody, mediaType); + + Request request = new Request.Builder() + .url(apiUrl) + .post(body) + .addHeader("accept", "application/json") + .addHeader("content-type", "application/json") + .addHeader("Authorization", "Bearer " + apiKey) + .build(); + + Response response = client.newCall(request).execute(); + + if (!response.isSuccessful()) { + throw new IOException("요청 실패: " + response.code()); + } + + byte[] imageBytes = response.body().bytes(); // ✅ 이미지 데이터를 byte[]로 변환 + response.close(); + return imageBytes; + } +} + diff --git a/src/main/java/com/bamboo/log/utils/ResponseHandler.java b/src/main/java/com/bamboo/log/utils/ResponseHandler.java new file mode 100644 index 0000000..c483a50 --- /dev/null +++ b/src/main/java/com/bamboo/log/utils/ResponseHandler.java @@ -0,0 +1,51 @@ +package com.bamboo.log.utils; + +import com.bamboo.log.utils.dto.DetailResponse; +import com.bamboo.log.utils.dto.ResponseForm; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class ResponseHandler { + + public static ResponseEntity create500Error(ResponseForm response, Exception e) { + response.of("result", "FAIL"); + response.of("error", DetailResponse.builder().code(500).message(e.getMessage()).build()); + return new ResponseEntity(response, HttpStatus.INTERNAL_SERVER_ERROR); + } + + public static ResponseEntity create400Error(ResponseForm response, Exception e) { + response.of("result", "FAIL"); + response.of("error", DetailResponse.builder().code(400).message(e.getMessage()).build()); + + return new ResponseEntity(response, HttpStatus.BAD_REQUEST); + } + + public static ResponseEntity create404Error(ResponseForm response, Exception e) { + response.of("result", "FAIL"); + response.of("error", DetailResponse.builder().code(404).message(e.getMessage()).build()); + + return new ResponseEntity(response, HttpStatus.NOT_FOUND); + } + + public static ResponseEntity create204Response(ResponseForm response, String message) { + response.of("result", "SUCCESS"); + response.of("code", DetailResponse.builder().code(204).message(message).build()); + + return new ResponseEntity(response, HttpStatus.NO_CONTENT); + } + + public static ResponseEntity create200Response(ResponseForm response, Object object) { + response.of("result", "SUCCESS"); + response.of("code", object); + + return new ResponseEntity(response, HttpStatus.OK); + } + + public static ResponseEntity create201Response(ResponseForm response, Object object) { + response.of("result", "SUCCESS"); + response.of("code", object); + + return new ResponseEntity(response, HttpStatus.CREATED); + } + +} diff --git a/src/main/java/com/bamboo/log/utils/SecurityConfig.java b/src/main/java/com/bamboo/log/utils/SecurityConfig.java new file mode 100644 index 0000000..5d51eb4 --- /dev/null +++ b/src/main/java/com/bamboo/log/utils/SecurityConfig.java @@ -0,0 +1,23 @@ +package com.bamboo.log.utils; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/images/**").permitAll() // 이미지 API 인증 없이 허용 + .anyRequest().authenticated() + ); + + return http.build(); + } +} diff --git a/src/main/java/com/bamboo/log/utils/dto/DetailResponse.java b/src/main/java/com/bamboo/log/utils/dto/DetailResponse.java new file mode 100644 index 0000000..61f22cf --- /dev/null +++ b/src/main/java/com/bamboo/log/utils/dto/DetailResponse.java @@ -0,0 +1,13 @@ +package com.bamboo.log.utils.dto; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class DetailResponse { + + private Integer code; + private String message; + +} \ No newline at end of file diff --git a/src/main/java/com/bamboo/log/utils/dto/ResponseForm.java b/src/main/java/com/bamboo/log/utils/dto/ResponseForm.java new file mode 100644 index 0000000..04eccf6 --- /dev/null +++ b/src/main/java/com/bamboo/log/utils/dto/ResponseForm.java @@ -0,0 +1,21 @@ +package com.bamboo.log.utils.dto; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +@Getter +public class ResponseForm { + + public ResponseForm() { + response = new HashMap<>(); + } + + public void of(String value1, Object value2) { + response.put(value1, value2); + } + + private Map response; + +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 356817f..317cea6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,13 +1,23 @@ spring: datasource: + driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_NAME} username: ${MYSQL_USERNAME} password: ${MYSQL_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver + jpa: show-sql: true open-in-view: false hibernate: ddl-auto: update properties: - hibernate.dialect: org.hibernate.dialect.MySQL8Dialect \ No newline at end of file + hibernate.dialect: org.hibernate.dialect.MySQL8Dialect + + +elice: + api: + token: ${API_TOKEN} + url: + face: ${FACE_API_URL} + img: ${IMG_API_URL} + chat: ${CHAT_API_URL}