Skip to content

Commit c61b432

Browse files
committed
feat : 백엔드 기능 완성
- Rest API 완성 - Firebase 기능 추가 - MessageQueue 기능 추가
1 parent 16d8ac3 commit c61b432

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+986
-95
lines changed

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
FROM openjdk:17-jdk-slim
2+
WORKDIR /app
3+
COPY build/libs/*.jar app.jar
4+
ENTRYPOINT ["java", "-jar", "app.jar"]

build.gradle

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ repositories {
2323
mavenCentral()
2424
}
2525

26+
2627
dependencies {
2728
// Swagger
2829
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8'
@@ -37,10 +38,10 @@ dependencies {
3738
runtimeOnly 'com.mysql:mysql-connector-j'
3839

3940
// QueryDSL
40-
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
41-
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
42-
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
43-
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
41+
//implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
42+
//annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
43+
//annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
44+
//annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
4445

4546
// Security + JWT
4647
implementation 'org.springframework.boot:spring-boot-starter-security'
@@ -60,8 +61,8 @@ dependencies {
6061
testCompileOnly 'org.projectlombok:lombok'
6162
testAnnotationProcessor 'org.projectlombok:lombok'
6263

63-
// Kafka (또는 Redis PubSub 선택 가능)
64-
implementation 'org.springframework.kafka:spring-kafka'
64+
// RabbitMQ
65+
implementation 'org.springframework.boot:spring-boot-starter-amqp'
6566

6667
// Test
6768
testImplementation 'org.springframework.boot:spring-boot-starter-test'
@@ -79,3 +80,7 @@ sourceSets {
7980
java.srcDirs += [querydslDir]
8081
}
8182
}
83+
84+
tasks.withType(JavaCompile).configureEach {
85+
options.encoding = "UTF-8"
86+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.vacation.homework.app.config;
2+
3+
import com.google.firebase.FirebaseApp;
4+
import com.google.firebase.FirebaseOptions;
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.context.annotation.Configuration;
7+
8+
import com.google.auth.oauth2.GoogleCredentials;
9+
import jakarta.annotation.PostConstruct;
10+
import org.springframework.core.io.ClassPathResource;
11+
12+
import java.io.FileInputStream;
13+
import java.io.IOException;
14+
import java.io.InputStream;
15+
import java.util.List;
16+
17+
@Configuration
18+
public class FirebaseConfig {
19+
20+
@Value("${firebase.key.path}")
21+
private String firebaseKeyPath;
22+
23+
@PostConstruct
24+
public void init() {
25+
try (InputStream serviceAccount = new FileInputStream(firebaseKeyPath)) {
26+
GoogleCredentials credentials = GoogleCredentials.fromStream(serviceAccount)
27+
.createScoped(List.of(
28+
"https://www.googleapis.com/auth/cloud-platform",
29+
"https://www.googleapis.com/auth/firebase.messaging"));
30+
31+
FirebaseOptions options = FirebaseOptions.builder()
32+
.setCredentials(credentials)
33+
.build();
34+
35+
if (FirebaseApp.getApps().isEmpty()) {
36+
FirebaseApp.initializeApp(options);
37+
System.out.println("✅ Firebase Admin SDK 초기화 성공");
38+
}
39+
} catch (IOException e) {
40+
throw new RuntimeException("❌ Firebase 초기화 실패", e);
41+
}
42+
}
43+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.vacation.homework.app.config;
2+
3+
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
4+
import org.springframework.amqp.core.Binding;
5+
import org.springframework.amqp.core.BindingBuilder;
6+
import org.springframework.amqp.core.DirectExchange;
7+
import org.springframework.amqp.rabbit.core.RabbitTemplate;
8+
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
9+
import org.springframework.context.annotation.Bean;
10+
import org.springframework.context.annotation.Configuration;
11+
import org.springframework.amqp.core.Queue;
12+
13+
@Configuration
14+
public class RabbitConfig {
15+
16+
@Bean
17+
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
18+
RabbitTemplate template = new RabbitTemplate(connectionFactory);
19+
template.setMessageConverter(new Jackson2JsonMessageConverter()); //배포환경에서도 JSON 처리 가능하도록
20+
return template;
21+
}
22+
23+
// === 기존 일기용 ===
24+
public static final String HOMEWORK_EXCHANGE = "homework.exchange";
25+
public static final String HOMEWORK_ROUTING_KEY = "homework.created";
26+
public static final String HOMEWORK_QUEUE = "homework.queue";
27+
28+
// === 신규 알림용 ===
29+
public static final String NOTIFICATION_EXCHANGE = "notification.exchange";
30+
public static final String NOTIFICATION_ROUTING_KEY = "notification.key";
31+
public static final String NOTIFICATION_QUEUE = "notification.queue";
32+
33+
34+
// === 기존 일기용 ===
35+
@Bean
36+
public DirectExchange exchange() {
37+
return new DirectExchange(HOMEWORK_EXCHANGE);
38+
}
39+
40+
@Bean
41+
public Queue queue() {
42+
return new Queue(HOMEWORK_QUEUE);
43+
}
44+
45+
@Bean
46+
public Binding binding(Queue queue, DirectExchange exchange) {
47+
return BindingBuilder.bind(queue).to(exchange).with(HOMEWORK_ROUTING_KEY);
48+
}
49+
50+
// === 신규 알림용 ===
51+
@Bean
52+
public DirectExchange notificationExchange() {
53+
return new DirectExchange(NOTIFICATION_EXCHANGE);
54+
}
55+
56+
@Bean
57+
public Queue notificationQueue() {
58+
return new Queue(NOTIFICATION_QUEUE);
59+
}
60+
61+
@Bean
62+
public Binding notificationBinding() {
63+
return BindingBuilder
64+
.bind(notificationQueue())
65+
.to(notificationExchange())
66+
.with(NOTIFICATION_ROUTING_KEY);
67+
}
68+
}

src/main/java/com/vacation/homework/app/config/SecurityConfig.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
public class SecurityConfig {
3131

3232
private final JwtUtil jwtUtil;
33+
private final JwtFilter jwtFilter;
3334
private final CustomUserDetailsService userDetailsService;
3435
private final CorsProperties corsProperties;
3536

@@ -42,6 +43,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
4243
.requestMatchers("/api/auth/**",
4344
"/api/users/**",
4445
"/api/terms/**",
46+
"/api/comment/**",
47+
4548
"/swagger-ui.html",
4649
"/swagger-ui/**",
4750
"/v3/api-docs/**",
@@ -54,6 +57,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
5457
.sessionManagement(session -> session
5558
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
5659
)
60+
//.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
5761
.addFilterBefore(new JwtAuthenticationFilter(jwtUtil, userDetailsService),
5862
UsernamePasswordAuthenticationFilter.class);
5963

src/main/java/com/vacation/homework/app/controller/AuthController.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.vacation.homework.app.dto.LoginRequest;
44
import com.vacation.homework.app.dto.LoginResponse;
5+
import com.vacation.homework.app.dto.base.DataResponseDto;
56
import com.vacation.homework.app.security.UserDetailsImpl;
67
import com.vacation.homework.app.service.AuthService;
78
import io.swagger.v3.oas.annotations.Operation;
@@ -20,8 +21,8 @@ public class AuthController {
2021
private final AuthService authService;
2122

2223
@GetMapping("/test")
23-
public ResponseEntity<String> login() {
24-
return ResponseEntity.ok("안녕하세요!");
24+
public DataResponseDto<String> login() {
25+
return DataResponseDto.of("안녕하세요!");
2526
}
2627

2728
@Operation(summary = "로그인", description = "아이디와 비밀번호로 로그인하고 JWT를 발급받습니다.")
@@ -30,15 +31,27 @@ public ResponseEntity<String> login() {
3031
@ApiResponse(responseCode = "401", description = "아이디 또는 비밀번호가 올바르지 않음")
3132
})
3233
@PostMapping("/login")
33-
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
34-
return ResponseEntity.ok(authService.login(request));
34+
public DataResponseDto<LoginResponse> login(@RequestBody LoginRequest request) {
35+
return DataResponseDto.of(authService.login(request));
3536
}
3637

3738
@Operation(summary = "로그아웃", description = "서버에 저장된 RefreshToken을 제거합니다.")
3839
@ApiResponse(responseCode = "204", description = "로그아웃 성공")
3940
@PostMapping("/logout")
40-
public ResponseEntity<Void> logout(@AuthenticationPrincipal UserDetailsImpl userDetails) {
41+
public DataResponseDto<Void> logout(@AuthenticationPrincipal UserDetailsImpl userDetails) {
4142
authService.logout(userDetails.getUserSeq());
42-
return ResponseEntity.noContent().build();
43+
return DataResponseDto.empty();
4344
}
45+
46+
@Operation(summary = "토큰 재발급", description = "리프레시 토큰을 통해 새로운 액세스 토큰을 발급받습니다.")
47+
@ApiResponses({
48+
@ApiResponse(responseCode = "200", description = "토큰 재발급 성공"),
49+
@ApiResponse(responseCode = "401", description = "리프레시 토큰이 유효하지 않음")
50+
})
51+
@PostMapping("/refresh")
52+
public DataResponseDto<LoginResponse> refresh(@RequestHeader("Authorization") String authHeader) {
53+
String refreshToken = authHeader.replace("Bearer ", "");
54+
return DataResponseDto.of(authService.refreshToken(refreshToken));
55+
}
56+
4457
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.vacation.homework.app.controller;
2+
3+
import com.vacation.homework.app.domain.Homework;
4+
import com.vacation.homework.app.dto.CreateCommentRequest;
5+
import com.vacation.homework.app.dto.CreateHomeworkRequest;
6+
import com.vacation.homework.app.dto.HomeworkDto;
7+
import com.vacation.homework.app.fcm.FcmService;
8+
import com.vacation.homework.app.security.UserDetailsImpl;
9+
import com.vacation.homework.app.service.CommentService;
10+
import com.vacation.homework.app.service.HomeworkService;
11+
import io.swagger.v3.oas.annotations.Operation;
12+
import io.swagger.v3.oas.annotations.media.Content;
13+
import io.swagger.v3.oas.annotations.media.Schema;
14+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
15+
import lombok.RequiredArgsConstructor;
16+
import org.springframework.beans.factory.annotation.Value;
17+
import org.springframework.http.ResponseEntity;
18+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
19+
import org.springframework.web.bind.annotation.*;
20+
21+
@RestController
22+
@RequestMapping("/api/comment")
23+
@RequiredArgsConstructor
24+
public class CommentController {
25+
26+
@Value("${ai.secret-key}")
27+
private String aiSecretKey;
28+
29+
private final CommentService commentService;
30+
private final HomeworkService homeworkService;
31+
32+
@Operation(summary = "AI가 일기 조회", description = "AI가 일기 내용을 조회합니다.")
33+
@ApiResponse(responseCode = "200", description = "일기 조회 성공", content = @Content(schema = @Schema(implementation = HomeworkDto.class)))
34+
@GetMapping
35+
public ResponseEntity<HomeworkDto> getHomework(
36+
@RequestHeader("X-AI-KEY") String headerKey,
37+
@RequestParam("userSeq") Long userSeq,
38+
@RequestParam("homeworkSeq") Long homeworkSeq) {
39+
if (!headerKey.equals(aiSecretKey)) throw new IllegalArgumentException("AI 인증 키가 유효하지 않습니다.");
40+
HomeworkDto dto = homeworkService.getHomework(userSeq, homeworkSeq);
41+
return ResponseEntity.ok(dto);
42+
}
43+
44+
45+
46+
@Operation(summary = "AI의 코멘트 저장", description = "AI의 코멘트를 저장합니다.")
47+
@ApiResponse(responseCode = "200", description = "AI코멘트 저장 성공", content = @Content(schema = @Schema(implementation = HomeworkDto.class)))
48+
@PostMapping
49+
public ResponseEntity<Void> createHomework(
50+
@RequestHeader("X-AI-KEY") String headerKey,
51+
@RequestBody CreateCommentRequest request) {
52+
if (!headerKey.equals(aiSecretKey)) throw new IllegalArgumentException("AI 인증 키가 유효하지 않습니다.");
53+
//코멘트 저장 및 알림전송
54+
commentService.saveComment(request.getUserSeq(), request.getHomeworkSeq(), request.getContent(), request.getSpellCheckResult());
55+
return ResponseEntity.noContent().build();
56+
}
57+
}

src/main/java/com/vacation/homework/app/controller/HomeworkController.java

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.vacation.homework.app.controller;
22

33
import com.vacation.homework.app.domain.Homework;
4-
import com.vacation.homework.app.domain.Weather;
54
import com.vacation.homework.app.dto.CreateHomeworkRequest;
65
import com.vacation.homework.app.dto.HomeworkDto;
6+
import com.vacation.homework.app.dto.base.DataResponseDto;
77
import com.vacation.homework.app.security.UserDetailsImpl;
88
import com.vacation.homework.app.service.HomeworkService;
99
import io.swagger.v3.oas.annotations.Operation;
@@ -28,32 +28,33 @@ public class HomeworkController {
2828
@Operation(summary = "일기 저장", description = "새 일기를 작성하여 저장합니다.")
2929
@ApiResponse(responseCode = "200", description = "일기 저장 성공", content = @Content(schema = @Schema(implementation = HomeworkDto.class)))
3030
@PostMapping
31-
public ResponseEntity<HomeworkDto> createHomework(
31+
public DataResponseDto<HomeworkDto> createHomework(
3232
@AuthenticationPrincipal UserDetailsImpl userDetails,
3333
@RequestBody CreateHomeworkRequest request) {
34-
homeworkService.saveHomework(
34+
Homework savedHomework = homeworkService.saveHomework(
3535
userDetails.getUserSeq(),
36+
request.getSelectedDate(),
3637
request.getTitle(),
3738
request.getContent(),
3839
request.getWeather(),
3940
request.getPhotoUrl()
4041
);
41-
return ResponseEntity.noContent().build();
42+
return DataResponseDto.empty();
4243
}
4344

4445
@Operation(summary = "일기 단건 조회", description = "homeworkSeq로 일기 상세정보를 조회합니다.")
4546
@GetMapping("/{homeworkSeq}")
46-
public ResponseEntity<HomeworkDto> getHomework(
47+
public DataResponseDto<HomeworkDto> getHomework(
4748
@AuthenticationPrincipal UserDetailsImpl userDetails,
4849
@PathVariable Long homeworkSeq) {
49-
Homework hw = homeworkService.getHomework(userDetails.getUserSeq(), homeworkSeq);
50-
return ResponseEntity.ok(HomeworkDto.from(hw));
50+
HomeworkDto dto = homeworkService.getHomework(userDetails.getUserSeq(), homeworkSeq);
51+
return DataResponseDto.of(dto);
5152
}
5253

5354

5455
@Operation(summary = "일기 월별 조회", description = "해당 유저의 특정 월 일기 목록을 조회합니다.")
5556
@GetMapping
56-
public ResponseEntity<List<HomeworkDto>> getHomeworksByMonth(
57+
public DataResponseDto<List<HomeworkDto>> getHomeworksByMonth(
5758
@AuthenticationPrincipal UserDetailsImpl userDetails,
5859
@RequestParam int year,
5960
@RequestParam int month) {
@@ -63,15 +64,15 @@ public ResponseEntity<List<HomeworkDto>> getHomeworksByMonth(
6364
.map(HomeworkDto::from)
6465
.collect(Collectors.toList());
6566

66-
return ResponseEntity.ok(list);
67+
return DataResponseDto.of(list);
6768
}
6869

69-
@Operation(summary = "일기 삭제", description = "해당 일기를 소프트 삭제합니다.")
70+
@Operation(summary = "일기 삭제", description = "해당 일기를 삭제합니다.")
7071
@DeleteMapping("/{homeworkSeq}")
71-
public ResponseEntity<Void> deleteHomework(
72+
public DataResponseDto<Void> deleteHomework(
7273
@AuthenticationPrincipal UserDetailsImpl userDetails,
7374
@PathVariable Long homeworkSeq) {
7475
homeworkService.deleteHomework(userDetails.getUserSeq(), homeworkSeq);
75-
return ResponseEntity.noContent().build();
76+
return DataResponseDto.empty();
7677
}
7778
}

src/main/java/com/vacation/homework/app/controller/TermsController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.vacation.homework.app.controller;
22

33
import com.vacation.homework.app.dto.TermsDto;
4+
import com.vacation.homework.app.dto.base.DataResponseDto;
45
import com.vacation.homework.app.service.TermsService;
56
import io.swagger.v3.oas.annotations.Operation;
67
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -20,7 +21,7 @@ public class TermsController {
2021
@Operation(summary = "최신 약관 조회", description = "현재 적용 중인 약관(version=app.terms.version)을 조회합니다.")
2122
@ApiResponse(responseCode = "200", description = "약관 조회 성공")
2223
@GetMapping("/latest")
23-
public ResponseEntity<List<TermsDto>> getLatestTerms() {
24-
return ResponseEntity.ok(termsService.getLatestTerms());
24+
public DataResponseDto<List<TermsDto>> getLatestTerms() {
25+
return DataResponseDto.of(termsService.getLatestTerms());
2526
}
2627
}

0 commit comments

Comments
 (0)