Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
"Bash(dir:*)",
"Bash(del /f \"C:\\Users\\haeun\\OneDrive\\Desktop\\BEProject\\konnect-back\\src\\main\\java\\com\\example\\konnect_backend\\domain\\auth\\dto\\request\\SignInRequest.java.bak\")",
"Bash(del /f \"C:\\Users\\haeun\\OneDrive\\Desktop\\BEProject\\konnect-back\\src\\main\\java\\com\\example\\konnect_backend\\domain\\ai\\service\\FileTranslationService.java\")",
"Bash(test:*)"
"Bash(test:*)",
"Bash(./gradlew bootRun:*)",
"Bash(curl:*)",
"Bash(./gradlew test:*)"
],
"deny": [],
"ask": []
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@ application-*.properties
# Google Vision API key
src/main/resources/google-key.json
google-key.json

src/main/resources/firebase/firebase-service-account.json
firebase-service-account.json
src/main/resources/firebase/*.json
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ dependencies {
// 캐싱
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
implementation 'org.springframework.boot:spring-boot-starter-cache'

// Firebase Admin SDK (FCM 푸시 알림)
implementation 'com.google.firebase:firebase-admin:9.2.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableJpaAuditing
@EnableScheduling
public class KonnectBackendApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,14 @@ public class DifficultExpressionExtractorModule implements PromptModule<String,

## 중요
- original: 원본 한국어 표현 그대로
- explanation: %s로 쉽게 풀어서 설명 (간단명료하게)
- explanation: %s로 쉽게 풀어서 설명 (간단명료하게, 순수 텍스트만)
- 최대 10개까지만 추출
- 정말 어려운 표현만 선별

## 출력 형식 규칙 (필수)
- 마크다운 문법 사용 금지 (###, **, *, -, |, 표 등 사용하지 않기)
- explanation 필드는 순수 텍스트로만 작성

## 분석할 텍스트
%s

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ public class DocumentClassifierModule implements PromptModule<String, Classifica
## 분석할 텍스트
%s

## 출력 형식 규칙 (필수)
- 마크다운 문법 사용 금지 (###, **, *, -, |, 표 등 사용하지 않기)
- 모든 텍스트 필드는 순수 텍스트로만 작성

## 응답 형식 (반드시 아래 JSON 형식으로만 출력, 다른 텍스트 없이)
분류 과정을 단계별로 설명해주세요:
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ public class KoreanSimplifierModule implements PromptModule<String, String> {
- 문서의 전체 내용과 구조는 유지
- 의미가 달라지지 않도록 주의

## 출력 형식 규칙 (필수)
- 마크다운 문법 사용 금지 (###, **, *, -, |, 표 등 사용하지 않기)
- 순수 텍스트로만 작성
- 줄바꿈은 허용하되, 특수 기호나 서식 없이 일반 문장으로 작성

## 원본 텍스트
%s

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ public class SummarizerModule implements PromptModule<String, String> {
- 학부모가 바로 이해할 수 있게 명확하게 작성
- %s로 요약문만 출력하고 다른 설명은 하지 마세요

## 출력 형식 규칙 (필수)
- 마크다운 문법 사용 금지 (###, **, *, -, |, 표 등 사용하지 않기)
- 순수 텍스트로만 작성
- 줄바꿈은 허용하되, 특수 기호나 서식 없이 일반 문장으로 작성

## 번역문
%s

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ public class TranslatorModule implements PromptModule<String, String> {
- 문단 구조 유지
- 번역문만 출력하고 다른 설명은 하지 마세요

## 출력 형식 규칙 (필수)
- 마크다운 문법 사용 금지 (###, **, *, -, |, 표 등 사용하지 않기)
- 순수 텍스트로만 작성
- 줄바꿈은 허용하되, 특수 기호나 서식 없이 일반 문장으로 작성

## 원문
%s

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ public class UnifiedExtractorModule implements PromptModule<String, ExtractionRe
- 모든 텍스트 필드는 반드시 %s로 작성해주세요
- 원본이 한국어여도 %s로 번역하여 출력

## 출력 형식 규칙 (필수)
- 마크다운 문법 사용 금지 (###, **, *, -, |, 표 등 사용하지 않기)
- 모든 텍스트 필드는 순수 텍스트로만 작성

## 오늘 날짜 (연도 추론에 활용)
%s

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.example.konnect_backend.domain.notification.controller;

import com.example.konnect_backend.domain.notification.dto.request.FcmTokenRequest;
import com.example.konnect_backend.domain.notification.dto.response.NotificationListResponse;
import com.example.konnect_backend.domain.notification.dto.response.UnreadCountResponse;
import com.example.konnect_backend.domain.notification.service.NotificationService;
import com.example.konnect_backend.global.ApiResponse;
import com.example.konnect_backend.global.security.SecurityUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/notifications")
@RequiredArgsConstructor
@Tag(name = "Notification", description = "알림 관리 API")
@SecurityRequirement(name = "bearerAuth")
public class NotificationController {

private final NotificationService notificationService;

@PostMapping("/fcm-token")
@Operation(summary = "FCM 토큰 등록", description = "푸시 알림을 위한 FCM 토큰을 등록합니다.")
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "토큰 등록 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "인증 실패",
content = @Content(schema = @Schema(implementation = ApiResponse.class)))
})
public ApiResponse<String> registerFcmToken(@Valid @RequestBody FcmTokenRequest fcmTokenRequest) {
Long userId = SecurityUtil.getCurrentUserIdOrNull();
notificationService.registerFcmToken(userId, fcmTokenRequest);
return ApiResponse.onSuccess("FCM 토큰이 등록되었습니다.");
}

@DeleteMapping("/fcm-token")
@Operation(summary = "FCM 토큰 삭제", description = "로그아웃 시 FCM 토큰을 삭제합니다.")
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "토큰 삭제 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "인증 실패",
content = @Content(schema = @Schema(implementation = ApiResponse.class)))
})
public ApiResponse<String> removeFcmToken(
@Parameter(description = "삭제할 FCM 토큰", required = true)
@RequestParam String token) {
notificationService.removeFcmToken(token);
return ApiResponse.onSuccess("FCM 토큰이 삭제되었습니다.");
}

@GetMapping
@Operation(summary = "알림 목록 조회", description = "사용자의 알림 목록을 페이징하여 조회합니다.")
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "인증 실패",
content = @Content(schema = @Schema(implementation = ApiResponse.class)))
})
public ApiResponse<NotificationListResponse> getNotifications(
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0")
@RequestParam(defaultValue = "0") int page,
@Parameter(description = "페이지 크기", example = "20")
@RequestParam(defaultValue = "20") int size) {
Long userId = SecurityUtil.getCurrentUserIdOrNull();
return ApiResponse.onSuccess(notificationService.getNotifications(userId, page, size));
}

@GetMapping("/unread-count")
@Operation(summary = "읽지 않은 알림 개수 조회", description = "읽지 않은 알림의 개수를 조회합니다.")
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "인증 실패",
content = @Content(schema = @Schema(implementation = ApiResponse.class)))
})
public ApiResponse<UnreadCountResponse> getUnreadCount() {
Long userId = SecurityUtil.getCurrentUserIdOrNull();
return ApiResponse.onSuccess(notificationService.getUnreadCount(userId));
}

@PatchMapping("/{notificationId}/read")
@Operation(summary = "알림 읽음 처리", description = "특정 알림을 읽음 처리합니다.")
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "읽음 처리 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "인증 실패",
content = @Content(schema = @Schema(implementation = ApiResponse.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "권한 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "알림을 찾을 수 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class)))
})
public ApiResponse<String> markAsRead(
@Parameter(description = "알림 ID", required = true, example = "1")
@PathVariable Long notificationId) {
Long userId = SecurityUtil.getCurrentUserIdOrNull();
notificationService.markAsRead(userId, notificationId);
return ApiResponse.onSuccess("알림이 읽음 처리되었습니다.");
}

@PatchMapping("/read-all")
@Operation(summary = "모든 알림 읽음 처리", description = "사용자의 모든 알림을 읽음 처리합니다.")
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "읽음 처리 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "인증 실패",
content = @Content(schema = @Schema(implementation = ApiResponse.class)))
})
public ApiResponse<String> markAllAsRead() {
Long userId = SecurityUtil.getCurrentUserIdOrNull();
notificationService.markAllAsRead(userId);
return ApiResponse.onSuccess("모든 알림이 읽음 처리되었습니다.");
}

@DeleteMapping("/{notificationId}")
@Operation(summary = "알림 삭제", description = "특정 알림을 삭제합니다.")
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "삭제 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "인증 실패",
content = @Content(schema = @Schema(implementation = ApiResponse.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "권한 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "알림을 찾을 수 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class)))
})
public ApiResponse<String> deleteNotification(
@Parameter(description = "알림 ID", required = true, example = "1")
@PathVariable Long notificationId) {
Long userId = SecurityUtil.getCurrentUserIdOrNull();
notificationService.deleteNotification(userId, notificationId);
return ApiResponse.onSuccess("알림이 삭제되었습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.konnect_backend.domain.notification.dto.request;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FcmTokenRequest {

@NotBlank(message = "FCM 토큰은 필수입니다")
private String token;

private String deviceId;

private String deviceType; // iOS, Android 등
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.konnect_backend.domain.notification.dto.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class NotificationListResponse {

private List<NotificationResponse> notifications;
private long unreadCount;
private int page;
private int size;
private long totalElements;
private int totalPages;
private boolean hasNext;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.example.konnect_backend.domain.notification.dto.response;

import com.example.konnect_backend.domain.notification.entity.Notification;
import com.example.konnect_backend.domain.notification.entity.NotificationType;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class NotificationResponse {

private Long id;
private String title;
private String body;
private NotificationType type;
private Long referenceId;
private Boolean isRead;

@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime createdAt;

public static NotificationResponse from(Notification notification) {
return NotificationResponse.builder()
.id(notification.getId())
.title(notification.getTitle())
.body(notification.getBody())
.type(notification.getType())
.referenceId(notification.getReferenceId())
.isRead(notification.getIsRead())
.createdAt(notification.getCreatedAt())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.konnect_backend.domain.notification.dto.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UnreadCountResponse {
private long unreadCount;
}
Loading