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
73 changes: 73 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# JWT
JWT_SECRET_KEY=your-jwt-secret-key-example-32chars!!
JWT_ACCESS_TOKEN_EXPIRATION_TIME=6000000

# Spring
SPRING_PROFILES_ACTIVE=dev

# Database
DATASOURCE_URL=jdbc:mysql://127.0.0.1:3306/koin?characterEncoding=utf8&useUnicode=true&mysqlEncoding=utf8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
DATASOURCE_USERNAME=koin
DATASOURCE_PASSWORD=your-db-password

# Redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=your-redis-password

# MongoDB
MONGODB_URI=mongodb://koin:your-mongo-password@127.0.0.1:27017/koin?authSource=admin

# Swagger
SWAGGER_SERVER_URL=http://localhost:8080

# AWS SES
AWS_SES_ACCESS_KEY=test
AWS_SES_SECRET_KEY=test

# S3
S3_BUCKET=your-bucket-name
S3_CUSTOM_DOMAIN=https://static.example.com/

# Slack
SLACK_KOIN_EVENT_NOTIFY_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
SLACK_KOIN_OWNER_EVENT_NOTIFY_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
SLACK_KOIN_SHOP_REVIEW_NOTIFY_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
SLACK_KOIN_LOST_ITEM_NOTIFY_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
SLACK_LOGGING_ERROR_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL

# Koin Admin
KOIN_ADMIN_SHOP_URL=https://admin.example.com/store
KOIN_ADMIN_REVIEW_URL=https://admin.example.com/review

# Open API
OPEN_API_KEY_PUBLIC=your-open-api-key-public
OPEN_API_KEY_TMONEY=your-open-api-key-tmoney

# FCM
FCM_KOIN_URL=example://

# Naver SMS
NAVER_ACCESS_KEY=your-naver-access-key
NAVER_SECRET_KEY=your-naver-secret-key
NAVER_SMS_API_URL=https://sens.apigw.ntruss.com
NAVER_SMS_SERVICE_ID=your-service-id
NAVER_SMS_FROM_NUMBER=01000000000

# KOIN VERIFICATION
MAX_VERIFICATION_COUNT=5

# CORS (콤마로 구분)
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080,https://example.com

# CloudFront
CLOUDFRONT_DISTRIBUTION_ID=test
CLOUDFRONT_REGION=ap-northeast-2

# Address API
ADDRESS_API_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
ADDRESS_API_KEY=your-address-api-key

# Toss Payment
TOSS_PAYMENT_SECRET_KEY=test_sk_your-toss-secret-key
TOSS_PAYMENT_API_BASE_URL=https://api.tosspayments.com/v1/payments
91 changes: 91 additions & 0 deletions .github/workflows/backend-cd-develop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: KOIN_API_V2 CD (develop)

on:
push:
branches:
- develop

jobs:
build-and-deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Record start time
run: echo "START_TIME=$(date +%s)" >> $GITHUB_ENV

- name: Notify Slack - Deploy Start
env:
ACTIONS_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
COMMIT_MSG=$(git log -1 --pretty=%s HEAD)
curl -X POST ${{ secrets.SLACK_DEPLOY_WEBHOOK_URL }} \
-H 'Content-Type: application/json' \
-d "{
\"text\": \":rocket: *[Develop] 배포 시작*\n• Repo: ${{ github.repository }}\n• Branch: develop\n• Author: @${{ github.actor }}\n• Commit: ${COMMIT_MSG}\n• <${ACTIONS_URL}|Actions 보기>\"
}"

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}

- name: Create Firebase Admin SDK JSON
run: echo '${{ secrets.FCM_ADMIN_SDK_JSON_DEVELOP }}' > src/main/resources/koin-firebase-adminsdk.json

- name: Build JAR
run: |
set -a
source .env.example
set +a
./gradlew clean build -x test -Dspring.profiles.active=dev

- name: SCP JAR to develop server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.DEVELOP_SERVER_HOST }}
username: ${{ secrets.DEVELOP_SERVER_USER }}
key: ${{ secrets.DEVELOP_SSH_PRIVATE_KEY }}
port: ${{ secrets.DEVELOP_SERVER_PORT }}
source: ${{ secrets.SOURCE_JAR_PATH }}
target: ${{ secrets.DEVELOP_SERVER_JAR_PATH }}
strip_components: 2

- name: Run deploy script on develop server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEVELOP_SERVER_HOST }}
username: ${{ secrets.DEVELOP_SERVER_USER }}
key: ${{ secrets.DEVELOP_SSH_PRIVATE_KEY }}
port: ${{ secrets.DEVELOP_SERVER_PORT }}
script: ${{ secrets.DEVELOP_DEPLOY_SCRIPT_PATH }}

- name: Notify Slack - Deploy Result
if: always()
env:
ACTIONS_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
COMMIT_MSG=$(git log -1 --pretty=%s HEAD)
DURATION_SEC=$(( $(date +%s) - START_TIME ))
DURATION="${DURATION_SEC}초 (약 $(( DURATION_SEC / 60 ))분)"
if [ "${{ job.status }}" = "success" ]; then
ICON=":white_check_mark:"
STATUS="배포 성공"
else
ICON=":x:"
STATUS="배포 실패"
fi
curl -X POST ${{ secrets.SLACK_DEPLOY_WEBHOOK_URL }} \
-H 'Content-Type: application/json' \
-d "{
\"text\": \"${ICON} *[Develop] ${STATUS}*\n• Repo: ${{ github.repository }}\n• Branch: develop\n• Author: @${{ github.actor }}\n• Commit: ${COMMIT_MSG}\n• Duration: ${DURATION}\n• <${ACTIONS_URL}|Actions 보기>\"
}"
89 changes: 89 additions & 0 deletions .github/workflows/backend-cd-main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: KOIN_API_V2 CD (main)

on:
workflow_dispatch:

jobs:
build-and-deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Record start time
run: echo "START_TIME=$(date +%s)" >> $GITHUB_ENV

- name: Notify Slack - Deploy Start
env:
ACTIONS_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
COMMIT_MSG=$(git log -1 --pretty=%s HEAD)
curl -X POST ${{ secrets.SLACK_DEPLOY_WEBHOOK_URL }} \
-H 'Content-Type: application/json' \
-d "{
\"text\": \":rocket: *[Production] 배포 시작*\n• Repo: ${{ github.repository }}\n• Branch: main\n• Author: @${{ github.actor }}\n• Commit: ${COMMIT_MSG}\n• <${ACTIONS_URL}|Actions 보기>\"
}"

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}

- name: Create Firebase Admin SDK JSON
run: echo '${{ secrets.FCM_ADMIN_SDK_JSON_MAIN }}' > src/main/resources/koin-firebase-adminsdk.json

- name: Build JAR
run: |
set -a
source .env.example
set +a
./gradlew clean build -x test -Dspring.profiles.active=prod

- name: SCP JAR to main server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.MAIN_SERVER_HOST }}
username: ${{ secrets.MAIN_SERVER_USER }}
key: ${{ secrets.MAIN_SSH_PRIVATE_KEY }}
port: ${{ secrets.MAIN_SERVER_PORT }}
source: ${{ secrets.SOURCE_JAR_PATH }}
target: ${{ secrets.MAIN_SERVER_JAR_PATH }}
strip_components: 2

- name: Run deploy script on main server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.MAIN_SERVER_HOST }}
username: ${{ secrets.MAIN_SERVER_USER }}
key: ${{ secrets.MAIN_SSH_PRIVATE_KEY }}
port: ${{ secrets.MAIN_SERVER_PORT }}
script: ${{ secrets.MAIN_DEPLOY_SCRIPT_PATH }}

- name: Notify Slack - Deploy Result
if: always()
env:
ACTIONS_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
COMMIT_MSG=$(git log -1 --pretty=%s HEAD)
DURATION_SEC=$(( $(date +%s) - START_TIME ))
DURATION="${DURATION_SEC}초 (약 $(( DURATION_SEC / 60 ))분)"
if [ "${{ job.status }}" = "success" ]; then
ICON=":white_check_mark:"
STATUS="배포 성공"
else
ICON=":x:"
STATUS="배포 실패"
fi
curl -X POST ${{ secrets.SLACK_DEPLOY_WEBHOOK_URL }} \
-H 'Content-Type: application/json' \
-d "{
\"text\": \"${ICON} *[Production] ${STATUS}*\n• Repo: ${{ github.repository }}\n• Branch: main\n• Author: @${{ github.actor }}\n• Commit: ${COMMIT_MSG}\n• Duration: ${DURATION}\n• <${ACTIONS_URL}|Actions 보기>\"
}"
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ out/
.vscode/
.DS_STORE

application.yml
*adminsdk.json
.env*
!.env.example

logs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package in.koreatech.koin.admin.callvan.controller;

import static in.koreatech.koin.domain.user.model.UserType.ADMIN;
import static in.koreatech.koin.global.code.ApiResponseCode.*;
import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH;

import org.springframework.http.ResponseEntity;
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;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import in.koreatech.koin.admin.callvan.dto.AdminCallvanReportProcessRequest;
import in.koreatech.koin.admin.callvan.dto.AdminCallvanReportsResponse;
import in.koreatech.koin.global.auth.Auth;
import in.koreatech.koin.global.code.ApiResponseCodes;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;

@Tag(name = "(Admin) Callvan: 신고 처리", description = "어드민 콜벤 사용자 신고 내역 관리")
@RequestMapping("/admin/callvan/reports")
public interface AdminCallvanReportApi {

@ApiResponseCodes({
OK,
UNAUTHORIZED_USER,
FORBIDDEN_ADMIN
})
@Operation(summary = "콜벤 신고 목록 조회", description = """
콜벤 사용자 신고 접수 목록을 조회합니다.
- `only_pending=true` 이면 미처리 신고(PENDING)만 조회합니다.
- 각 항목에는 피신고자 정보, 신고 사유, 첨부 이미지, 처리 유형, 누적 신고 이력이 포함됩니다.
""")
@GetMapping
ResponseEntity<AdminCallvanReportsResponse> getCallvanReports(
@RequestParam(name = "only_pending", required = false, defaultValue = "false") Boolean onlyPending,
@RequestParam(name = "page", required = false) Integer page,
@RequestParam(name = "limit", required = false) Integer limit,
@Auth(permit = {ADMIN}) Integer adminId);

@ApiResponseCodes({
OK,
UNAUTHORIZED_USER,
FORBIDDEN_ADMIN,
INVALID_REQUEST_BODY,
NOT_FOUND_CALLVAN_REPORT,
CALLVAN_REPORT_ALREADY_PROCESSED
})
@Operation(summary = "콜벤 신고 처리", description = """
콜벤 신고를 처리합니다.
- `WARNING`: 신고 확정 후 주의 안내 알림을 발송합니다.
- `TEMPORARY_RESTRICTION_14_DAYS`: 신고 확정 후 14일간 새 모집/참여를 제한하며, 제재 알림을 발송합니다.
- `PERMANENT_RESTRICTION`: 신고 확정 후 콜벤 기능을 영구 제한하며, 제재 알림을 발송합니다.
- `REJECT`: 신고를 반려하고 상태를 REJECTED로 변경합니다. 알림은 발송되지 않습니다.

#### 제재 유형별 알림 메시지
| 처리 유형 | 알림 타입 | 메시지 |
| :--- | :--- | :--- |
| `WARNING` | `REPORT_WARNING` | 콜벤팟 이용 과정에서 신고가 접수되어 운영 검토 후 주의 안내가 전달되었습니다. 이후 동일한 문제가 반복될 경우 콜벤 기능 이용이 제한될 수 있습니다. |
| `TEMPORARY_RESTRICTION_14_DAYS` | `REPORT_RESTRICTION_14_DAYS` | 콜벤팟 이용 과정에서 신고가 접수되어 운영 검토 후 14일간 콜벤 기능 이용이 제한되었습니다. |
| `PERMANENT_RESTRICTION` | `REPORT_PERMANENT_RESTRICTION` | 콜벤팟 이용 과정에서 신고가 접수되어 운영 검토 후 콜벤 기능 이용이 영구적으로 제한되었습니다. |
""")
@PostMapping("/{reportId}/process")
ResponseEntity<Void> processCallvanReport(
@Parameter(in = PATH) @PathVariable Integer reportId,
@RequestBody @Valid AdminCallvanReportProcessRequest request,
@Auth(permit = {ADMIN}) Integer adminId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package in.koreatech.koin.admin.callvan.controller;

import static in.koreatech.koin.domain.user.model.UserType.ADMIN;
import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH;

import org.springframework.http.ResponseEntity;
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;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import in.koreatech.koin.admin.callvan.dto.AdminCallvanReportProcessRequest;
import in.koreatech.koin.admin.callvan.dto.AdminCallvanReportsResponse;
import in.koreatech.koin.admin.callvan.service.AdminCallvanReportQueryService;
import in.koreatech.koin.admin.callvan.service.AdminCallvanReportService;
import in.koreatech.koin.global.auth.Auth;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/admin/callvan/reports")
@RequiredArgsConstructor
public class AdminCallvanReportController implements AdminCallvanReportApi {

private final AdminCallvanReportService adminCallvanReportService;
private final AdminCallvanReportQueryService adminCallvanReportQueryService;

@GetMapping
public ResponseEntity<AdminCallvanReportsResponse> getCallvanReports(
@RequestParam(name = "only_pending", required = false, defaultValue = "false") Boolean onlyPending,
@RequestParam(name = "page", required = false) Integer page,
@RequestParam(name = "limit", required = false) Integer limit,
@Auth(permit = {ADMIN}) Integer adminId
) {
return ResponseEntity.ok(adminCallvanReportQueryService.getReports(onlyPending, page, limit));
}

@PostMapping("/{reportId}/process")
public ResponseEntity<Void> processCallvanReport(
@Parameter(in = PATH) @PathVariable Integer reportId,
@RequestBody @Valid AdminCallvanReportProcessRequest request,
@Auth(permit = {ADMIN}) Integer adminId
) {
adminCallvanReportService.processReport(reportId, adminId, request);
return ResponseEntity.ok().build();
}
}
Loading
Loading