Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
a3c9a37
#250 [Fix] 투표 비율 계산 기준을 preVoteOption에서 postVoteOption으로 수정 (#252)
HYH0804 May 27, 2026
a38c141
#254 [Refactor] 관점 등록/수정 시 GPT 검수 코드 제거 (#256)
HYH0804 Jun 3, 2026
cee2c5d
#255 [Refactor] VoteStatsResponse OptionStat에 title 필드 추가 (#257)
HYH0804 Jun 3, 2026
53533b8
#258 [Feat] 출석체크 API 추가
si-zero Jun 3, 2026
77db7a1
#260 [Feat] 스웨거 어드민/유저 분리
si-zero Jun 4, 2026
a9d4293
[Feat] 알림기능 (#263)
HYH0804 Jun 11, 2026
b5a681b
#262 [Fix] APNs 자격증명 S3 다운로드 시 임시파일 충돌 오류 수정
HYH0804 Jun 11, 2026
d281fa3
#264 [Fix] AdMob SSV 포인트 미적립 버그 수정
si-zero Jun 11, 2026
968d6a3
#262 merge (#266)
HYH0804 Jun 14, 2026
d041631
[Chore] adMob 크롤링 파일 설정: adMob 크롤링 파일 설정 #269
si-zero Jun 16, 2026
ec25882
#270 [Feat] 설정 파일 추가
si-zero Jun 16, 2026
4c45999
[Chore] JWT필터 설정 #271
si-zero Jun 16, 2026
e0c9c1c
#275 [Fix] AdMob SSV 콜백 파라미터 optional 처리 및 서명 검증 활성화
si-zero Jun 18, 2026
ab2cb78
#277 [Fix] AdMob SSV 콜백 URL 검증 요청 처리 실패
si-zero Jun 18, 2026
04d947c
#279 [Fix] AdMob SSV 엔드포인트 검증 실패 시 항상 200 반환
si-zero Jun 18, 2026
d9ea3b5
#283 [Fix] /me/content-activities voteSide null 반환 버그
si-zero Jun 19, 2026
ec27b01
#285 [Feat] content-activities 응답에 option_title 필드 추가
si-zero Jun 20, 2026
6663e7a
#287 [Fix] AdMob SSV 서명 검증 오류로 인한 포인트 미지급 버그
si-zero Jun 24, 2026
39aca84
#289 [Fix] 오늘의 배틀 개수 변경
si-zero Jul 5, 2026
11e2da1
#293 [Fix] 사전투표 철학자 아이콘 미노출
si-zero Jul 5, 2026
aa449f0
#297 [Fix] 댓글 작성 시각이 9시간 전으로 표시되는 버그
si-zero Jul 6, 2026
3259a1a
#301 [Fix] robots.txt 파일 부재로 AdMob app-ads.txt 크롤링 차단
si-zero Jul 6, 2026
e91819b
#305 [Fix] S3 오디오 조각 재다운로드 시 FileAlreadyExistsException으로 TTS 파이프라인 실패
si-zero Jul 6, 2026
d64d5ca
#309 [Fix] JwtFilter 자체 화이트리스트에 /robots.txt 누락으로 401 발생
si-zero Jul 6, 2026
ff16d85
#316 [Fix] 포인트 정책표 대비 코드 불일치 4건 수정
si-zero Jul 7, 2026
f50c749
Merge branch 'main' into dev
si-zero Jul 7, 2026
d9e28a1
#321 [Feat] 알림 미읽음 여부 확인 API 추가
si-zero Jul 7, 2026
4279341
#313 [Hotfix] Railway 배포 환경 FFmpeg 자동 설치 적용 (#320)
jucheonsu Jul 7, 2026
f77048c
#327 [Fix] 알림 전체 읽음 처리 후 모두 읽음 신호가 전달되지 않던 문제 수정
si-zero Jul 13, 2026
99886f9
#331 [Docs] 출석체크 API 명세서를 실제 코드에 맞춰 갱신
si-zero Jul 13, 2026
087b8c0
#333 [Fix] app-ads.txt가 text/html로 응답되어 AdMob 크롤러가 무시하는 문제 수정
si-zero Jul 13, 2026
d33ff1f
#337 [Feat] 인앱 알림 테스트용 API 추가
si-zero Jul 14, 2026
17f35a4
#341 [Fix] 미문서화/중복 포인트 지급 로직 정리
si-zero Jul 15, 2026
acbe078
관리자 페이지를 새 admin-web(Next.js) 앱으로 리다이렉트 (#346)
Jul 15, 2026
de75bdf
#349 [Feat] admin.web.url env 주입 + 기본값 picke.up.railway.app (#350)
Jul 15, 2026
9493351
#352 [Fix] JwtFilter 화이트리스트 /api/v1/auth 전체 매칭으로 logout 인증 우회, 500 에러 발생
si-zero Jul 16, 2026
00bfcb3
#356 [Feat] 이용약관/개인정보처리방침 페이지 백엔드 서빙
si-zero Jul 20, 2026
99c936d
#361 [Feat] 관리자용 테스트 계정 로컬 로그인 기능 추가
si-zero Jul 24, 2026
f0a837d
#360 [Refactor] 1인 1관점 제한을 해제하고 1인 다수 관점 등록 허용
si-zero Jul 24, 2026
9fb601f
#366 [Feat] 내 콘텐츠 활동 내역(댓글 탭)에 내가 작성한 관점 포함
si-zero Jul 24, 2026
6a1abde
#370 [Fix] 사전투표만 한 기록도 내 배틀 기록에 노출되는 버그
si-zero Jul 24, 2026
80f5b9c
[Feat] 관점 200자 제한 해제 (#375)
si-zero Aug 2, 2026
f9bc278
#378 [Feat] 관리자 예약 푸시알림(제목/소제목/발송시간) 설정 및 자동발송
si-zero Aug 8, 2026
99f4765
#382 [Fix] 내 콘텐츠 활동 댓글 정보 매핑 오류 수정 (#383)
jucheonsu Aug 9, 2026
79cb355
#387 [Refactor] 철학자 태그 제한 및 가치관 육각형 계산 보정 (#388)
jucheonsu Aug 13, 2026
bdbe3b3
#391 [Remove] 미사용 파일 정리 (비활성 CI 워크플로우 + 레거시 admin 프론트)
si-zero Aug 13, 2026
a82c575
#392 [CI/CD] Java CI 파이프라인 브랜치명 오류 및 지속적 빌드 실패 수정
si-zero Aug 13, 2026
70ccc90
#395 [Feat] 관리자용 푸시 알림 발송 테스트 API 추가
si-zero Aug 16, 2026
2e72bf7
#396 [Fix] Android FCM 푸시에 title/body가 실리지 않아 알림이 안 뜨던 문제 수정
si-zero Aug 16, 2026
1fc8488
#401 [Fix] FCM 만료된 디바이스 토큰이 정리되지 않아 반복 발송 실패 로그가 쌓이는 문제 수정
si-zero Aug 17, 2026
a4d3b31
#405 [Feat] 어드민 예약 알림 On/Off 전환 API 추가 및 관리자 알림 API 문서화
si-zero Aug 26, 2026
0b9a374
#407 [Feat] 어드민 유저 검색 API 추가
si-zero Aug 26, 2026
869de77
#409 [Feat] 어드민 공지사항 수정/삭제 API 추가
si-zero Aug 26, 2026
087fea5
#411 [Feat] 어드민 공지 발송 결과 조회 API 추가
si-zero Aug 26, 2026
4bdbdf3
#413 [Feat] 어드민 공지 발송 대상자 수 미리보기 API 추가
si-zero Aug 26, 2026
76e8097
#414 [Feat] 어드민 공지 작성 옵션(카테고리) 조회 API 추가
si-zero Aug 26, 2026
26116e8
#415 [Feat] 어드민 예약 알림 테스트 발송 API 추가
si-zero Aug 26, 2026
7362b43
Merge remote-tracking branch 'origin/main' into merge/#419
si-zero Aug 26, 2026
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
223 changes: 222 additions & 1 deletion docs/api-specs/notification-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,10 +334,231 @@

---

## 6. 에러 코드
## 6. 관리자 API

모든 API는 `ROLE_ADMIN` 권한을 가진 계정만 호출 가능합니다 (`Authorization: Bearer {access_token}`, 403 시 `COMMON_403` 등 공통 권한 에러).

### 6.1 공지/이벤트 (`/api/v1/admin/notices`)

즉시 발송되는 공지사항/이벤트 알림을 관리합니다.

| Method | Path | 설명 |
|---|---|---|
| `POST` | `/api/v1/admin/notices` | 공지사항/이벤트 작성 (즉시 전체 발송) |
| `GET` | `/api/v1/admin/notices/options` | 공지 작성 시 선택 가능한 `category` 목록 조회 |
| `GET` | `/api/v1/admin/notices/target-count` | 지금 등록하면 몇 대의 디바이스에 발송되는지 미리 조회 |
| `GET` | `/api/v1/admin/notices` | 공지 목록 조회 (`category`, `page`, `size` 쿼리) |
| `GET` | `/api/v1/admin/notices/{noticeId}` | 공지 상세 조회 |
| `PUT` | `/api/v1/admin/notices/{noticeId}` | 공지 수정 (제목/본문) |
| `DELETE` | `/api/v1/admin/notices/{noticeId}` | 공지 삭제 |
| `GET` | `/api/v1/admin/notices/{noticeId}/delivery-result` | 발송 결과 조회 (대상/성공/실패 건수) |
| `POST` | `/api/v1/admin/notices/test` | 특정 유저 대상 테스트 푸시 발송 (알림 설정 ON/OFF 무관) |

**`POST /api/v1/admin/notices` 요청 바디**

```json
{
"category": "NOTICE",
"title": "서비스 점검 안내",
"body": "8/30 02:00~04:00 점검이 진행됩니다."
}
```

| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `category` | `string` | Y | `CONTENT` \| `NOTICE` \| `EVENT` |
| `title` | `string` | Y | 알림 제목 |
| `body` | `string` | Y | 알림 본문 |

응답은 `AdminNoticeDetailResponse` (`notificationId`, `category`, `detailCode`, `title`, `body`, `referenceId`, `createdAt`).

**`GET /api/v1/admin/notices/options`**

공지 작성 폼의 카테고리 선택지를 서버 enum 기준으로 내려줍니다. `ALL`은 목록 조회 필터용이라 작성 옵션에는 포함되지 않습니다.

성공 응답 `200 OK`:

```json
{
"statusCode": 200,
"data": {
"categories": ["CONTENT", "NOTICE", "EVENT"]
},
"error": null
}
```

**`GET /api/v1/admin/notices/target-count`**

`NOTICE`/`EVENT` 공지를 지금 등록하면 몇 대의 디바이스에 발송되는지 미리 조회합니다. `notifyAdminNotice`가 사용하는 것과 동일하게 '이벤트 및 소식 알림' 설정이 ON인 유저의 등록된 디바이스 수 기준입니다. `CONTENT` 카테고리 공지는 애초에 이 대상자 산정을 타지 않으므로 참고용으로만 사용하세요.

성공 응답 `200 OK`:

```json
{
"statusCode": 200,
"data": {
"targetCount": 1200
},
"error": null
}
```

**`PUT /api/v1/admin/notices/{noticeId}` 요청 바디**

```json
{
"title": "서비스 점검 안내 (수정)",
"body": "8/30 02:00~05:00로 점검 시간이 변경되었습니다."
}
```

| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `title` | `string` | Y | 알림 제목 |
| `body` | `string` | Y | 알림 본문 |

`category`/`detailCode`는 등록 시점에 확정되어 수정 대상이 아닙니다. 이미 발송된 알림함/푸시는 재발송되지 않고, 알림함에 남아있는 텍스트만 갱신됩니다. 응답은 등록 API와 동일한 `AdminNoticeDetailResponse`.

**`DELETE /api/v1/admin/notices/{noticeId}`**

성공 시 `200 OK`, `data: null`. 삭제된 공지는 알림함에서도 사라집니다.

**`GET /api/v1/admin/notices/{noticeId}/delivery-result`**

`CONTENT` 카테고리를 제외한 `NOTICE`/`EVENT` 공지처럼 실제로 푸시가 발송되는 알림에 한해, 발송 대상 디바이스 수와 성공/실패 건수를 조회합니다. 푸시 발송은 Android(FCM)는 동기, iOS(APNs)는 비동기로 처리되므로, 전체 발송이 끝나기 전에는 `pending: true`로 내려갑니다.

성공 응답 `200 OK`:

```json
{
"statusCode": 200,
"data": {
"notificationId": 101,
"targetCount": 1200,
"successCount": 1180,
"failureCount": 20,
"pending": false
},
"error": null
}
```

`failureCount`는 만료된 디바이스 토큰(`UNREGISTERED`, APNs의 `BadDeviceToken`/`Unregistered` 등) 정리 대상 건수를 포함한 전체 실패 건수이며, 실패 사유별 세부 분류는 제공하지 않습니다.

예외 응답 `404 - 발송 결과 없음` (공지 자체가 없거나, `CONTENT` 카테고리처럼 관리자 발송 트리거를 거치지 않아 결과가 집계되지 않은 경우):

```json
{
"statusCode": 404,
"data": null,
"error": {
"code": "NOTIFICATION_404_DELIVERY_RESULT",
"message": "발송 결과가 집계되지 않은 알림입니다."
}
}
```

**`POST /api/v1/admin/notices/test` 요청 바디**

```json
{
"userId": 123,
"title": "테스트 알림",
"body": "테스트 발송입니다."
}
```

### 6.2 예약 알림 (`/api/v1/admin/notification-schedules`)

매일 지정된 시각에 전체 유저에게 자동 발송되는 예약 알림(`NotificationSchedule`)을 관리합니다. 매분 `NotificationScheduleDispatcher`가 `enabled=true`인 예약 중 `sendTime`(시:분)이 현재 시각과 일치하는 건을 찾아 발송하며, 같은 날 중복 발송되지 않도록 `lastSentDate`로 발송 이력을 관리합니다.

| Method | Path | 설명 |
|---|---|---|
| `POST` | `/api/v1/admin/notification-schedules` | 예약 알림 등록 |
| `GET` | `/api/v1/admin/notification-schedules` | 예약 알림 전체 목록 조회 |
| `GET` | `/api/v1/admin/notification-schedules/{scheduleId}` | 예약 알림 상세 조회 |
| `PUT` | `/api/v1/admin/notification-schedules/{scheduleId}` | 예약 알림 수정 (제목/부제목/발송시간/on-off 전체 교체) |
| `PATCH` | `/api/v1/admin/notification-schedules/{scheduleId}/toggle` | 예약 알림 On/Off만 전환 |
| `POST` | `/api/v1/admin/notification-schedules/{scheduleId}/test` | 저장된 제목/부제목으로 특정 유저에게 테스트 발송 |
| `DELETE` | `/api/v1/admin/notification-schedules/{scheduleId}` | 예약 알림 삭제 |

**`POST` / `PUT` 요청 바디**

```json
{
"title": "오늘의 질문",
"subtitle": "지금 확인해보세요",
"sendTime": "19:00:00",
"enabled": true
}
```

| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `title` | `string` | Y | 알림 제목 |
| `subtitle` | `string` | Y | 알림 부제목(본문) |
| `sendTime` | `string` (`HH:mm:ss`) | Y | 매일 발송할 시각 (KST 기준, 분 단위로 매칭) |
| `enabled` | `boolean` | Y | 활성화 여부 |

**`PATCH .../toggle` 요청 바디**

```json
{
"enabled": false
}
```

**`POST .../test` 요청 바디**

```json
{
"userId": 123
}
```

저장되어 있는 예약 알림의 `title`/`subtitle`을 그대로 사용해, 지정한 유저의 등록된 디바이스로 알림 설정(ON/OFF) 무관하게 즉시 테스트 푸시를 발송합니다. 등록/수정 폼에서 저장 전 미리보기 용도로 쓰려면, 먼저 원하는 문구로 `POST`/`PUT`을 호출해 저장한 뒤 이 API로 확인하는 흐름을 권장합니다.

**응답 (`AdminNotificationScheduleResponse`)**

```json
{
"statusCode": 200,
"data": {
"id": 3,
"title": "오늘의 질문",
"subtitle": "지금 확인해보세요",
"sendTime": "19:00:00",
"enabled": true,
"createdAt": "2026-08-20T10:00:00"
},
"error": null
}
```

목록 조회(`GET /api/v1/admin/notification-schedules`) 응답은 `{ "schedules": [AdminNotificationScheduleResponse, ...] }` 형태입니다.

예외 응답 `404 - 예약 알림 없음`:

```json
{
"statusCode": 404,
"data": null,
"error": {
"code": "NOTIFICATION_404_SCHEDULE",
"message": "존재하지 않는 알림 예약입니다."
}
}
```

---

## 7. 에러 코드

| Error Code | HTTP Status | 설명 |
|---|:---:|---|
| `COMMON_400` | `400` | 요청 파라미터가 잘못되었습니다. (예: `fcmToken`/`platform` 누락) |
| `USER_404` | `404` | 존재하지 않는 사용자입니다. |
| `NOTIFICATION_404` | `404` | 존재하지 않는 알림입니다. (본인 소유가 아닌 알림 포함) |
| `NOTIFICATION_404_SCHEDULE` | `404` | 존재하지 않는 예약 알림입니다. |
| `NOTIFICATION_404_DELIVERY_RESULT` | `404` | 발송 결과가 집계되지 않은 알림입니다. |
44 changes: 41 additions & 3 deletions docs/api-specs/user-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,9 +438,47 @@

---

## 4. 에러 코드
## 4. 관리자 API

### 4.1 공통 에러 코드
`ROLE_ADMIN` 권한을 가진 계정만 호출 가능합니다 (`Authorization: Bearer {access_token}`).

### 4.1 `GET /api/v1/admin/users/search`

닉네임 / 유저태그 / 이메일로 유저를 검색합니다. 어드민 페이지에서 특정 유저에게 테스트 알림을 보내는 등, 내부 `userId`를 알아야 하는 다른 관리자 API의 입력값을 찾기 위한 용도입니다.

이메일은 소셜 로그인 유저의 `providerEmail`만 검색 대상이며, **로컬 로그인 유저는 이메일 필드 자체가 없어 닉네임/유저태그로만 검색**됩니다.

쿼리 파라미터:

| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `keyword` | `string` | Y | 닉네임/유저태그/이메일 중 하나라도 부분 일치하면 매칭 |
| `page` | `integer` | N | 페이지 번호 (기본값 `0`) |
| `size` | `integer` | N | 페이지 크기 (기본값 `20`) |

성공 응답 `200 OK`:

```json
{
"statusCode": 200,
"data": {
"items": [
{ "userId": 123, "userTag": "picke_abcd", "nickname": "민초러버", "email": "user@gmail.com" },
{ "userId": 124, "userTag": "picke_efgh", "nickname": "로컬유저", "email": null }
],
"hasNext": false
},
"error": null
}
```

`email`은 소셜 계정이 없는 유저(로컬 로그인)의 경우 `null`로 내려갑니다.

---

## 5. 에러 코드

### 5.1 공통 에러 코드

| Error Code | HTTP Status | 설명 |
|------------|:-----------:|------|
Expand All @@ -452,7 +490,7 @@
| `USER_SUSPENDED` | `403` | 일정 기간 이용 정지된 사용자 |
| `INTERNAL_SERVER_ERROR` | `500` | 서버 오류 |

### 4.2 사용자 에러 코드
### 5.2 사용자 에러 코드

| Error Code | HTTP Status | 설명 |
|------------|:-----------:|------|
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.swyp.picke.domain.admin.controller;

import com.swyp.picke.domain.admin.dto.notification.request.AdminNoticeCreateRequest;
import com.swyp.picke.domain.admin.dto.notification.request.AdminNoticeUpdateRequest;
import com.swyp.picke.domain.admin.dto.notification.request.AdminNotificationTestRequest;
import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeDeliveryResultResponse;
import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeDetailResponse;
import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeListResponse;
import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeOptionsResponse;
import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeTargetCountResponse;
import com.swyp.picke.domain.admin.service.AdminNotificationService;
import com.swyp.picke.domain.notification.enums.NotificationCategory;
import com.swyp.picke.domain.notification.service.NotificationDispatchService;
Expand All @@ -13,9 +17,11 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
Expand All @@ -39,6 +45,12 @@ public ApiResponse<AdminNoticeDetailResponse> createNotice(
return ApiResponse.onSuccess(adminNotificationService.createNotice(request));
}

@Operation(summary = "공지사항 작성 옵션 조회", description = "공지 작성 시 선택 가능한 category 목록을 내려준다.")
@GetMapping("/options")
public ApiResponse<AdminNoticeOptionsResponse> getOptions() {
return ApiResponse.onSuccess(adminNotificationService.getOptions());
}

@Operation(summary = "공지사항 목록 조회")
@GetMapping
public ApiResponse<AdminNoticeListResponse> getNotices(
Expand All @@ -49,12 +61,40 @@ public ApiResponse<AdminNoticeListResponse> getNotices(
return ApiResponse.onSuccess(adminNotificationService.getNotices(category, page, size));
}

@Operation(summary = "공지 발송 대상자 수 미리보기", description = "지금 NOTICE/EVENT 공지를 등록하면 몇 대의 디바이스에 발송되는지 미리 조회한다.")
@GetMapping("/target-count")
public ApiResponse<AdminNoticeTargetCountResponse> getTargetCount() {
return ApiResponse.onSuccess(adminNotificationService.getTargetCount());
}

@Operation(summary = "공지사항 상세 조회")
@GetMapping("/{noticeId}")
public ApiResponse<AdminNoticeDetailResponse> getNoticeDetail(@PathVariable Long noticeId) {
return ApiResponse.onSuccess(adminNotificationService.getNoticeDetail(noticeId));
}

@Operation(summary = "공지사항 수정", description = "이미 발송된 알림함/푸시는 재발송되지 않으며, 알림함에 남는 텍스트만 갱신된다.")
@PutMapping("/{noticeId}")
public ApiResponse<AdminNoticeDetailResponse> updateNotice(
@PathVariable Long noticeId,
@RequestBody @Valid AdminNoticeUpdateRequest request
) {
return ApiResponse.onSuccess(adminNotificationService.updateNotice(noticeId, request));
}

@Operation(summary = "공지사항 삭제")
@DeleteMapping("/{noticeId}")
public ApiResponse<Void> deleteNotice(@PathVariable Long noticeId) {
adminNotificationService.deleteNotice(noticeId);
return ApiResponse.onSuccess(null);
}

@Operation(summary = "공지사항 발송 결과 조회", description = "대상 디바이스 수, 성공/실패 건수를 조회한다. 푸시 발송은 비동기로 이뤄지므로 완료 전에는 pending=true로 내려간다.")
@GetMapping("/{noticeId}/delivery-result")
public ApiResponse<AdminNoticeDeliveryResultResponse> getDeliveryResult(@PathVariable Long noticeId) {
return ApiResponse.onSuccess(adminNotificationService.getDeliveryResult(noticeId));
}

@Operation(summary = "푸시 알림 발송 테스트", description = "특정 유저의 등록된 디바이스로 알림 설정(ON/OFF) 무관하게 즉시 테스트 푸시를 발송한다.")
@PostMapping("/test")
public ApiResponse<Void> sendTestPush(
Expand Down
Loading
Loading