Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0762d72
Feat: 컴퓨터공학부 대학원 공지 Scrap 추가 (#291)
jiyun921 Sep 12, 2025
ed1721d
Feat/cors setting
zbqmgldjfh Sep 13, 2025
054e092
Feat: 유저 학사일정 알림 설정 관련 기본 설정 추가 (#302)
rlagkswn00 Sep 20, 2025
675e8a1
Feat: 학사일정 알림 설정 API 추가 (#303)
rlagkswn00 Sep 21, 2025
34a700a
Merge branch 'main' into develop
rlagkswn00 Sep 21, 2025
656d52b
fix: 교직원 스크랩 DTO변환간 중복 Key 문제 해결
rlagkswn00 Sep 21, 2025
61244aa
fix: 불필요 로깅 삭제
rlagkswn00 Sep 21, 2025
ebae030
fix: 불필요 주석 삭제
rlagkswn00 Sep 21, 2025
afe4b04
Feat: 학사일정 알림 스케쥴링 로직 추가 (#305)
rlagkswn00 Sep 26, 2025
bbfd2e6
Fix: 학과 공지 업데이트 오류 수정, 학과 목록 조회 api 수정 (#310)
jiyun921 Oct 6, 2025
f910b06
Feat: OCI 환경 CI/CD배포 파이프라인 추가 (#311)
rlagkswn00 Oct 7, 2025
9bb94a9
fix: dev 환경 ci/cd workflow 수정(decrpt 추가 및 sh 명령어 수정) (#312)
rlagkswn00 Oct 7, 2025
9e9e8a2
feat: decrpt secrets 실행 시 env 추가 (#313)
rlagkswn00 Oct 7, 2025
485d77a
Feat: 학사일정 카테고리 및 알림 전송 분류 추가 (#308)
rlagkswn00 Oct 7, 2025
2a5e030
Feat: 어드민 기능- 전체 사용자 토픽 구독 API 추가 (#314)
rlagkswn00 Oct 8, 2025
871565d
Fix: 학사일정 알림 및 업데이트 시간 수정 (#315)
rlagkswn00 Oct 8, 2025
f2bfeb3
Feat: 대학원 스크랩 Feature Flag 적용 (#316)
rlagkswn00 Oct 8, 2025
beb2476
Merge branch 'main' into develop
rlagkswn00 Oct 8, 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
85 changes: 51 additions & 34 deletions .github/workflows/dev.yml
Original file line number Diff line number Diff line change
@@ -1,54 +1,71 @@
# This is a basic workflow to help you get started with Actions
name: Deploy to OCI (Develop)

name: Deploy to develop

# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [ develop ]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
deploy:
name: Build & Deploy to OCI (Dev)
runs-on: ubuntu-latest
environment: Test-Server

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
# (1) 코드 체크아웃
- name: Checkout repository
uses: actions/checkout@v3

# (2) JDK 17 설치
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
Comment on lines +15 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

GitHub Actions 최신 런타임에 맞게 액션 버전 업데이트 필요
actions/checkout@v3actions/setup-java@v3는 Node 16 런타임에 의존하는 구버전이라 현재 GitHub 호스티드 러너에서 실행 즉시 실패합니다(actionlint에서도 동일하게 지적됨). 배포 파이프라인이 막히지 않도록 v4로 올려 주세요.

-      - name: Checkout repository
-        uses: actions/checkout@v3
+      - name: Checkout repository
+        uses: actions/checkout@v4
...
-      - name: Set up JDK 17
-        uses: actions/setup-java@v3
+      - name: Set up JDK 17
+        uses: actions/setup-java@v4
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uses: actions/checkout@v3
# (2) JDK 17 설치
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Checkout repository
uses: actions/checkout@v4
# (2) JDK 17 설치
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
🧰 Tools
🪛 actionlint (1.7.7)

15-15: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)


19-19: the runner of "actions/setup-java@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🤖 Prompt for AI Agents
In .github/workflows/dev.yml around lines 15 to 22, the workflow uses
actions/checkout@v3 and actions/setup-java@v3 which depend on an older Node 16
runtime and fail on current GitHub hosted runners; update both action references
to their v4 releases (e.g., actions/checkout@v4 and actions/setup-java@v4) so
they use the latest runtime compatibility, then commit the updated workflow
file.


# (3) firebase secret decrypt
- name: Decrypt Secrets
env:
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
sh .github/workflows/decrypt.sh

- name: Ready to deploy
# (4) Gradle 빌드
- name: Build project
run: |
rm .gitignore
git config user.email "[email protected]"
git config user.name "ngwoon"
git add .
git commit -m "Build Ready"
./gradlew clean build -x test

- name: Install Heroku CLI
run: |
curl https://cli-assets.heroku.com/install.sh | sh
# (5) 빌드 결과 확인
- name: Verify JAR
run: ls -al build/libs

- name: Deploy to Heroku
# You may pin to the exact commit or the version.
# uses: AkhileshNS/heroku-deploy@79ef2ae4ff9b897010907016b268fd0f88561820
uses: AkhileshNS/[email protected]
# (6) OCI 서버로 JAR 파일 전송
- name: Upload to OCI
uses: appleboy/[email protected]
with:
# This will be used for authentication. You can find it in your heroku homepage account settings
heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
# Email that you use with heroku
heroku_email: ${{ secrets.HEROKU_EMAIL }}
# The appname to use for deploying/updating
heroku_app_name: ${{ secrets.HEROKU_APP_NAME }}
env:
HD_DEPLOY_ENV: "dev"
host: ${{ secrets.OCI_DEV_HOST }}
username: ubuntu
key: ${{ secrets.OCI_DEV_SSH_KEY }}
source: "build/libs/*.jar"
target: "/home/ubuntu/dev-app"

# (7) SSH 접속 → 기존 프로세스 중지 → 새 버전 실행
- name: Restart Dev Application
uses: appleboy/[email protected]
with:
host: ${{ secrets.OCI_DEV_HOST }}
username: ubuntu
key: ${{ secrets.OCI_DEV_SSH_KEY }}
script: |
echo "[1] 현재 실행중인 개발 서버 종료 중..."
PID=$(ps -ef | grep 'kuring-.*\.jar' | grep -v grep | awk '{print $2}')
if [ -n "$PID" ]; then
echo "기존 프로세스 종료: $PID"
kill -9 $PID
else
echo "기존 Java 프로세스 없음"
fi

echo "[2] 새 JAR 실행"
cd /home/ubuntu/dev-app
JAR_FILE=$(ls -t build/libs/*.jar | grep -m1 -v 'plain')
nohup java -jar "$JAR_FILE" --spring.profiles.active=dev > /home/ubuntu/dev-app.log 2>&1 &
echo "✅ 개발 서버 재시작 완료!"
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import com.kustacks.kuring.common.utils.converter.StringToDateTimeConverter;
import com.kustacks.kuring.common.utils.validator.BadWordInitProcessor;
import com.kustacks.kuring.common.utils.validator.WhitelistWordInitProcessor;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
Expand All @@ -28,7 +27,6 @@
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand Down Expand Up @@ -136,11 +134,12 @@ public ResponseEntity<BaseResponse<String>> refreshWhitelistWords() {
return ResponseEntity.ok().body(new BaseResponse<>(ResponseCodeAndMessages.ADMIN_LOAD_WHITELIST_WORDS, null));
}

@Hidden
@Operation(summary = "사용자 토픽 재구독", description = "모든 사용자의 구독 정보를 기반으로 Firebase 토픽을 재구독합니다")
@SecurityRequirement(name = "JWT")
@Secured(AdminRole.ROLE_ROOT)
@GetMapping("/subscribe/all")
public ResponseEntity<Void> subscribe() {
adminCommandUseCase.subscribeAllUserSameTopic();
return ResponseEntity.ok().build();
@PostMapping("/users/subscriptions/all")
public ResponseEntity<BaseResponse<String>> resubscribeAllUsersToTopics() {
adminCommandUseCase.resubscribeAllUsersToTopics();
return ResponseEntity.ok().body(new BaseResponse<>(ResponseCodeAndMessages.ADMIN_USER_SUBSCRIPTION_UPDATE_SUCCESS, null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ public interface AdminCommandUseCase {

void createRealNoticeForAllUser(RealNotificationCommand command);

void subscribeAllUserSameTopic();

void addAlertSchedule(AlertCreateCommand command);

void cancelAlertSchedule(Long id);

void embeddingCustomData(DataEmbeddingCommand command);

void resubscribeAllUsersToTopics();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;

public interface AdminUserFeedbackPort {
List<String> findAllToken();

Page<FeedbackDto> findAllFeedbackByPageRequest(Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package com.kustacks.kuring.admin.application.service;

import com.google.firebase.messaging.FirebaseMessaging;
import com.kustacks.kuring.admin.application.port.in.AdminCommandUseCase;
import com.kustacks.kuring.admin.application.port.in.dto.RealNotificationCommand;
import com.kustacks.kuring.admin.application.port.in.dto.TestNotificationCommand;
import com.kustacks.kuring.admin.application.port.out.AdminAlertEventPort;
import com.kustacks.kuring.admin.application.port.out.AdminEventPort;
import com.kustacks.kuring.admin.application.port.out.AdminUserFeedbackPort;
import com.kustacks.kuring.admin.application.port.out.AiEventPort;
import com.kustacks.kuring.admin.domain.Admin;
import com.kustacks.kuring.alert.application.port.in.dto.AlertCreateCommand;
import com.kustacks.kuring.alert.application.port.in.dto.DataEmbeddingCommand;
import com.kustacks.kuring.auth.userdetails.UserDetailsServicePort;
import com.kustacks.kuring.common.annotation.UseCase;
import com.kustacks.kuring.common.properties.ServerProperties;
import com.kustacks.kuring.message.application.port.out.FirebaseSubscribePort;
import com.kustacks.kuring.notice.domain.CategoryName;
import com.kustacks.kuring.user.application.port.out.UserQueryPort;
import com.kustacks.kuring.user.domain.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.InputStreamResource;
Expand All @@ -26,8 +27,12 @@
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ACADEMIC_EVENT_TOPIC;
import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ALL_DEVICE_SUBSCRIBED_TOPIC;

@Slf4j
Expand All @@ -36,10 +41,11 @@
public class AdminCommandService implements AdminCommandUseCase {

private final UserDetailsServicePort userDetailsServicePort;
private final AdminUserFeedbackPort adminUserFeedbackPort;
private final AdminAlertEventPort adminAlertEventPort;
private final AdminEventPort adminEventPort;
private final AiEventPort aiEventPort;
private final UserQueryPort userQueryPort;
private final FirebaseSubscribePort firebaseSubscribePort;
private final NoticeProperties noticeProperties;
private final ServerProperties serverProperties;
private final PasswordEncoder passwordEncoder;
Expand Down Expand Up @@ -107,21 +113,61 @@ public void embeddingCustomData(DataEmbeddingCommand command) {
}
}

/**
* TODO : 1회성 API - client v2 배포 후, 단 한번 모든 사용자를 공통 topic에 구독시킨 후 제거 예정
*/
@Transactional
@Override
public void subscribeAllUserSameTopic() {
String topic = serverProperties.ifDevThenAddSuffix(ALL_DEVICE_SUBSCRIBED_TOPIC);
public void resubscribeAllUsersToTopics() {
List<User> allUsers = userQueryPort.findAllWithSubscriptions();
Map<String, List<String>> topicSubscriptions = new HashMap<>();

//User 당 (1 + 학사일정 알림 구독여부 + 유저별 카테고리 구독 수 + 유저별 학과 구독 수) 반복
//User 당 최대 2 + 카테고리 수 + 학과 수 => 넉넉 잡아 80
//ex. User 5000명 * 80 => 400,000번 반복
for (User user : allUsers) {
String fcmToken = user.getFcmToken();

// allDevice 토픽 - 모든 사용자
String allDeviceTopic = serverProperties.ifDevThenAddSuffix(ALL_DEVICE_SUBSCRIBED_TOPIC);
topicSubscriptions.computeIfAbsent(allDeviceTopic, k -> new LinkedList<>()).add(fcmToken);

// academicEvent 토픽 - 학사일정 알림이 활성화된 사용자
if (user.isAcademicEventNotificationEnabled()) {
String academicEventTopic = serverProperties.ifDevThenAddSuffix(ACADEMIC_EVENT_TOPIC);
topicSubscriptions.computeIfAbsent(academicEventTopic, k -> new LinkedList<>()).add(fcmToken);
}

// 카테고리별 토픽
user.getSubscribedCategoryList().forEach(category -> {
String categoryTopic = serverProperties.ifDevThenAddSuffix(category.getName());
topicSubscriptions.computeIfAbsent(categoryTopic, k -> new LinkedList<>()).add(fcmToken);
});

// 학과별 토픽
user.getSubscribedDepartmentList().forEach(department -> {
String departmentTopic = serverProperties.ifDevThenAddSuffix(department.getName());
topicSubscriptions.computeIfAbsent(departmentTopic, k -> new LinkedList<>()).add(fcmToken);
});
}

// 토픽별로 500개씩 나누어 구독 처리
for (Map.Entry<String, List<String>> entry : topicSubscriptions.entrySet()) {
String topic = entry.getKey();
List<String> tokens = entry.getValue();

log.info("Resubscribing {} users to topic: {}", tokens.size(), topic);

int successCount = 0;
int failureCount = 0;

FirebaseMessaging instance = FirebaseMessaging.getInstance();
List<String> allToken = adminUserFeedbackPort.findAllToken();
for (int i = 0; i < tokens.size(); i += 500) {
List<String> batch = tokens.subList(i, Math.min(i + 500, tokens.size()));
try {
firebaseSubscribePort.subscribeToTopic(batch, topic);
successCount += batch.size();
} catch (Exception e) {
failureCount += batch.size();
}
}

int size = allToken.size();
for (int i = 0; i < size; i += 500) {
List<String> subList = allToken.subList(i, Math.min(i + 500, size));
instance.subscribeToTopicAsync(subList, topic);
log.info("Resubscribed {} users to topic: {}. {} users failed.", successCount, topic, failureCount);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.IOException;
import java.util.stream.Collectors;

import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ACADEMIC_EVENT_TOPIC;
import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ALL_DEVICE_SUBSCRIBED_TOPIC;

@Slf4j
Expand Down Expand Up @@ -59,14 +60,26 @@ private void register(String userFcmToken) {
log.warn("User already exists: {}", userFcmToken, e);
}

UserSubscribeCommand command =
new UserSubscribeCommand(
userFcmToken,
serverProperties.ifDevThenAddSuffix(ALL_DEVICE_SUBSCRIBED_TOPIC)
);
subscribeDefaultTopics(userFcmToken);
}

private void subscribeDefaultTopics(String userFcmToken) {
subscribeTopic(userFcmToken, ALL_DEVICE_SUBSCRIBED_TOPIC);
subscribeTopic(userFcmToken, ACADEMIC_EVENT_TOPIC);
}

private void subscribeTopic(String userFcmToken, String topic) {
UserSubscribeCommand command = makeSubscribeCommand(userFcmToken, topic);
firebaseService.subscribe(command);
}

private UserSubscribeCommand makeSubscribeCommand(String userFcmToken, String topic) {
return new UserSubscribeCommand(
userFcmToken,
serverProperties.ifDevThenAddSuffix(topic)
);
}

public String convert(HttpServletRequest request) throws IOException {
String content = request.getReader().lines()
.collect(Collectors.joining(System.lineSeparator()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setString(1, academicEvent.getEventUid());
ps.setString(2, academicEvent.getSummary());
ps.setString(3, academicEvent.getDescription());
ps.setString(4, academicEvent.getCategory());
ps.setString(4, academicEvent.getCategory().name());
ps.setString(5, academicEvent.getTransparent().toString());
ps.setInt(6, academicEvent.getSequence());
ps.setBoolean(7, academicEvent.getNotifyEnabled());
Expand Down Expand Up @@ -68,7 +68,7 @@ public void setValues(PreparedStatement ps, int i) throws SQLException {
AcademicEvent academicEvent = events.get(i);
ps.setString(1, academicEvent.getSummary());
ps.setString(2, academicEvent.getDescription());
ps.setString(3, academicEvent.getCategory());
ps.setString(3, academicEvent.getCategory().name());
ps.setString(4, academicEvent.getTransparent().toString());
ps.setInt(5, academicEvent.getSequence());
ps.setBoolean(6, academicEvent.getNotifyEnabled());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,9 @@ public List<AcademicEventReadModel> findEventsAfter(LocalDate startDate) {
public List<AcademicEventReadModel> findEventsBefore(LocalDate endDate) {
return findEventsBetweenDate(null, endDate);
}

@Override
public List<AcademicEventReadModel> findTodayEvents(LocalDate date) {
return academicEventRepository.findTodayEvents(date);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ interface AcademicEventQueryRepository {
List<AcademicEventReadModel> findAllEventReadModels();

List<AcademicEventReadModel> findEventsByDate(LocalDate startDate, LocalDate endDate);

List<AcademicEventReadModel> findTodayEvents(LocalDate date);
}
Loading
Loading